From d57259c9be388f95fd7b0357b730ecb569e525fc Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 2 Jun 2017 08:26:55 -0700 Subject: [PATCH] Initial working version of password auth --- .travis.yml | 6 +- LICENSE | 27 +++ bin/generate-methods.js | 18 +- bin/template/enum.tmpl | 4 +- bin/update-proto.js | 43 +++-- package.json | 3 +- proto/auth.proto | 6 +- proto/kv.proto | 4 +- proto/rpc.proto | 52 +++--- src/auth.ts | 195 ++++++++++++++++++++++ src/backoff/backoff.ts | 2 - src/backoff/exponential.ts | 6 +- src/builder.ts | 156 ++++++------------ src/connection-pool.ts | 180 +++++++++++++++----- src/errors.ts | 41 ++++- src/index.ts | 56 ++++++- src/lease.ts | 16 +- src/lock.ts | 3 +- src/memoize.ts | 68 ++++++++ src/range.ts | 112 +++++++++++++ src/rpc.ts | 124 +++++++------- src/shared-pool.ts | 4 +- src/util.ts | 15 +- test/_setup.ts | 2 +- test/backoff.test.ts | 2 +- test/certs/certs/ca.crt | 30 ++++ test/certs/certs/etcd-client.crt | 117 +++++++++++++ test/certs/certs/etcd0.localhost.crt | 119 ++++++++++++++ test/certs/etcd-client.csr | 28 ++++ test/certs/etcd0.localhost.csr | 28 ++++ test/certs/newcerts/01.pem | 119 ++++++++++++++ test/certs/newcerts/02.pem | 117 +++++++++++++ test/certs/private/ca.key | 52 ++++++ test/certs/private/etcd-client.key | 52 ++++++ test/certs/private/etcd0.localhost.key | 52 ++++++ test/certs/readme.md | 1 + test/{kv.test.ts => client.test.ts} | 219 ++++++++++++++++++++++--- test/connection-pool.test.ts | 20 ++- test/memoize.test.ts | 69 ++++++++ test/range.test.ts | 55 +++++++ test/util.ts | 20 ++- tsconfig.json | 1 + tslint.json | 85 +++++++--- 43 files changed, 1996 insertions(+), 333 deletions(-) create mode 100644 LICENSE create mode 100644 src/auth.ts create mode 100644 src/memoize.ts create mode 100644 src/range.ts create mode 100644 test/certs/certs/ca.crt create mode 100644 test/certs/certs/etcd-client.crt create mode 100644 test/certs/certs/etcd0.localhost.crt create mode 100644 test/certs/etcd-client.csr create mode 100644 test/certs/etcd0.localhost.csr create mode 100644 test/certs/newcerts/01.pem create mode 100644 test/certs/newcerts/02.pem create mode 100644 test/certs/private/ca.key create mode 100644 test/certs/private/etcd-client.key create mode 100644 test/certs/private/etcd0.localhost.key create mode 100644 test/certs/readme.md rename test/{kv.test.ts => client.test.ts} (59%) create mode 100644 test/memoize.test.ts create mode 100644 test/range.test.ts diff --git a/.travis.yml b/.travis.yml index fb49567..2a82a38 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,5 +7,9 @@ before_install: - curl -L https://github.com/coreos/etcd/releases/download/${ETCD_VER}/etcd-${ETCD_VER}-linux-amd64.tar.gz -o /tmp/etcd-${ETCD_VER}-linux-amd64.tar.gz - mkdir -p /tmp/etcd - tar xzvf /tmp/etcd-${ETCD_VER}-linux-amd64.tar.gz -C /tmp/etcd --strip-components=1 - - /tmp/etcd/etcd > /dev/null & + - /tmp/etcd/etcd + --advertise-client-urls https://127.0.0.1:2379 + --listen-client-urls https://127.0.0.1:2379 + --cert-file ${TRAVIS_BUILD_DIR}/test/certs/certs/etcd0.localhost.crt + --key-file ${TRAVIS_BUILD_DIR}/test/certs/private/etcd0.localhost.key > /dev/null & - npm i -g npm diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..82f3065 --- /dev/null +++ b/LICENSE @@ -0,0 +1,27 @@ +MIT License + +Copyright (c) Microsoft + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--- + +Some code, where noted, is imported from the coreos/etcd project. This code +is made available under the Apache 2.0 license, which can be read here: +https://www.apache.org/licenses/LICENSE-2.0.txt diff --git a/bin/generate-methods.js b/bin/generate-methods.js index 596eafe..98195ae 100644 --- a/bin/generate-methods.js +++ b/bin/generate-methods.js @@ -32,6 +32,7 @@ const pbTypeAliases = { bool: 'boolean', string: 'string', bytes: 'Buffer', + Type: 'Permission', }; const numericTypes = [ @@ -82,6 +83,7 @@ function template(name, params) { getCommentPrefixing, getLineContaining, formatType, + aliases: pbTypeAliases, }); emit(templates[name](params).replace(/^\-\- *\n/gm, '').replace(/^\-\-/gm, '')); @@ -95,7 +97,7 @@ function stripPackageNameFrom(name) { return name; } -function formatType(type, isInResponse = false) { +function formatTypeInner(type, isInResponse) { if (type in pbTypeAliases) { return pbTypeAliases[type]; } @@ -113,13 +115,25 @@ function formatType(type, isInResponse = false) { return `I${type}`; } +function formatType(type, isInResponse = false) { + const isEnum = enums.includes(type); + const formatted = formatTypeInner(type, isInResponse); + + // grpc unmarshals enums as their string representations. + if (isEnum) { + return isInResponse ? `keyof typeof ${formatted}` : `${formatted} | keyof typeof ${formatted}`; + } + + return formatted; +} + function getLineContaining(substring, from = 0) { return lines.findIndex((l, i) => i >= from && l.includes(substring)); } function indent(level) { let out = ''; - for (let i = 0; i < level; i++) { + for (let i = 0; i < level; i += 1) { out += indentation; } return out; diff --git a/bin/template/enum.tmpl b/bin/template/enum.tmpl index 67c0670..d4fcd04 100644 --- a/bin/template/enum.tmpl +++ b/bin/template/enum.tmpl @@ -1,6 +1,6 @@ -export enum <%= name %> { +export enum <%= name in aliases ? aliases[name] : name %> { --<% _.forOwn(node.values, (count, field) => { %> --<%= getCommentPrefixing(`${field} = ${count}`, getLineContaining(`enum ${name}`)) %> - <%= field %> = <%= count %>, + <%= _.camelCase(field) %> = <%= count %>, --<% }) %> } diff --git a/bin/update-proto.js b/bin/update-proto.js index e66e066..7a8cb89 100644 --- a/bin/update-proto.js +++ b/bin/update-proto.js @@ -9,21 +9,11 @@ * */ +const changeCase = require('change-case'); const fetch = require('node-fetch'); const path = require('path'); const fs = require('fs'); -/** - * Matches lines that should be stripped out from the combined proto file. - * @type {RegExp[]} - */ -const ignores = [ - /^import .+/, - /^option .+/, - /^package .+/, - /^syntax .+/, -]; - /** * Files to fetch and concatenate. * @type {String[]} @@ -43,6 +33,34 @@ const files = [ }, ]; +/** + * Matches lines that should be stripped out from the combined proto file. + * @type {RegExp[]} + */ +const ignores = [ + /^import .+/, + /^option .+/, + /^package .+/, + /^syntax .+/, +]; + +/** + * Filters out lines that should be ignored when transforming the proto files. + */ +const filterRemovedLines = line => !ignores.some(re => re.test(line)); + +const uppercaseEnumFieldRe = /^(\s*)([A-Z_]+)(\s*=\s*[0-9]+;.*)$/; + +/** + * Etcd provides all enums as UPPER_CASE. We change them to cameCase here + * to match TypeScript conventions better. + */ +function lowerCaseEnumFields(line) { + return line.replace(uppercaseEnumFieldRe, (_match, indentation, name, value) => { + return `${indentation}${changeCase.camelCase(name)}${value}`; + }); +} + const baseUrl = 'https://raw.githubusercontent.com/coreos/etcd/master'; Promise.all(files.map(f => { @@ -51,7 +69,8 @@ Promise.all(files.map(f => { .then(contents => { return 'syntax = "proto3";\n' + f.prefix + contents .split(/\r?\n/g) - .filter(line => !ignores.some(re => re.test(line))) + .filter(filterRemovedLines) + .map(lowerCaseEnumFields) .join('\n') .replace(/\n\n+/g, '\n'); }) diff --git a/package.json b/package.json index 0cd9c46..ca642d5 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "test": "npm-run-all --parallel test:lint test:unit", "test:unit": "mocha --compilers ts:ts-node/register --timeout 20000 -r test/_setup.ts test/*.test.ts", "test:lint": "tslint --type-check --project tsconfig.json '{src,test}/**/*.ts'", - "update-proto": "node ./bin/update-proto ./proto && node bin/generate-methods.js ./proto/rpc.proto > src/rpc.ts", + "build:proto": "node ./bin/update-proto ./proto && node bin/generate-methods.js ./proto/rpc.proto > src/rpc.ts", "build:doc": "rm -rf docs && typedoc --exclude \"**/test/*\" --excludePrivate --out ./docs ./src/index.ts && node bin/tame-typedoc", "build:ts": "tsc && cp -R proto lib", "prepublish": "npm run -s build:ts" @@ -39,6 +39,7 @@ "@types/sinon": "^2.1.2", "chai": "^3.5.0", "chai-subset": "^1.5.0", + "change-case": "^3.0.1", "lodash": "^4.17.4", "mocha": "^3.2.0", "node-fetch": "^1.6.3", diff --git a/proto/auth.proto b/proto/auth.proto index 0e19466..6e98cf4 100644 --- a/proto/auth.proto +++ b/proto/auth.proto @@ -10,9 +10,9 @@ message User { // Permission is a single entity message Permission { enum Type { - READ = 0; - WRITE = 1; - READWRITE = 2; + read = 0; + write = 1; + readwrite = 2; } Type permType = 1; bytes key = 2; diff --git a/proto/kv.proto b/proto/kv.proto index 5499eb8..fabdc71 100644 --- a/proto/kv.proto +++ b/proto/kv.proto @@ -21,8 +21,8 @@ message KeyValue { } message Event { enum EventType { - PUT = 0; - DELETE = 1; + put = 0; + delete = 1; } // type is the kind of event. If type is a PUT, it indicates // new data has been stored to the key. If type is a DELETE, diff --git a/proto/rpc.proto b/proto/rpc.proto index ff652e5..e4897ac 100644 --- a/proto/rpc.proto +++ b/proto/rpc.proto @@ -292,16 +292,16 @@ message ResponseHeader { } message RangeRequest { enum SortOrder { - NONE = 0; // default, no sorting - ASCEND = 1; // lowest target value first - DESCEND = 2; // highest target value first + none = 0; // default, no sorting + ascend = 1; // lowest target value first + descend = 2; // highest target value first } enum SortTarget { - KEY = 0; - VERSION = 1; - CREATE = 2; - MOD = 3; - VALUE = 4; + key = 0; + version = 1; + create = 2; + mod = 3; + value = 4; } // key is the first key for the range. If range_end is not given, the request only looks up key. bytes key = 1; @@ -418,16 +418,16 @@ message ResponseOp { } message Compare { enum CompareResult { - EQUAL = 0; - GREATER = 1; - LESS = 2; - NOT_EQUAL = 3; + equal = 0; + greater = 1; + less = 2; + notEqual = 3; } enum CompareTarget { - VERSION = 0; - CREATE = 1; - MOD = 2; - VALUE= 3; + version = 0; + create = 1; + mod = 2; + value= 3; } // result is logical comparison operation for this comparison. CompareResult result = 1; @@ -537,9 +537,9 @@ message WatchCreateRequest { bool progress_notify = 4; enum FilterType { // filter out put event. - NOPUT = 0; + noput = 0; // filter out delete event. - NODELETE = 1; + nodelete = 1; } // filters filter the events at server side before it sends back to the watcher. repeated FilterType filters = 5; @@ -641,6 +641,8 @@ message MemberAddResponse { ResponseHeader header = 1; // member is the member information for the added member. Member member = 2; + // members is a list of all members after adding the new member. + repeated Member members = 3; } message MemberRemoveRequest { // ID is the member ID of the member to remove. @@ -648,6 +650,8 @@ message MemberRemoveRequest { } message MemberRemoveResponse { ResponseHeader header = 1; + // members is a list of all members after removing the member. + repeated Member members = 2; } message MemberUpdateRequest { // ID is the member ID of the member to update. @@ -657,6 +661,8 @@ message MemberUpdateRequest { } message MemberUpdateResponse{ ResponseHeader header = 1; + // members is a list of all members after updating the member. + repeated Member members = 2; } message MemberListRequest { } @@ -671,14 +677,14 @@ message DefragmentResponse { ResponseHeader header = 1; } enum AlarmType { - NONE = 0; // default, used to query if any alarm is active - NOSPACE = 1; // space quota is exhausted + none = 0; // default, used to query if any alarm is active + nospace = 1; // space quota is exhausted } message AlarmRequest { enum AlarmAction { - GET = 0; - ACTIVATE = 1; - DEACTIVATE = 2; + get = 0; + activate = 1; + deactivate = 2; } // action is the kind of alarm request to issue. The action // may GET alarm statuses, ACTIVATE an alarm, or DEACTIVATE a diff --git a/src/auth.ts b/src/auth.ts new file mode 100644 index 0000000..9259066 --- /dev/null +++ b/src/auth.ts @@ -0,0 +1,195 @@ +import { Range } from './range'; +import { AuthClient, Permission } from './rpc'; +import { toBuffer } from './util'; + +/** + * IPermission can be used to grant a certain role in etcd access to a certain + * key range, or prefix. + */ +export type IPermissionRequest = + { permission: keyof typeof Permission, range: Range } | + { permission: keyof typeof Permission, key: Buffer | string }; + +function getRange(req: IPermissionRequest): Range { + if (req.hasOwnProperty('key')) { + return new Range(toBuffer((<{ key: Buffer | string }> req).key)); + } + + return (<{ range: Range }> req).range; +} + +/** + * IGrant is used for granting a permission to a user. + */ +export interface IPermissionResult { + permission: keyof typeof Permission; + range: Range; +} + +/** + * The Role provides an entry point for managing etcd roles. Etcd has an + * ACL-esque system: users have one or more roles, and roles have one or + * more permissions that grant them access (read, write, or both) on key + * ranges. + */ +export class Role { + constructor( + private client: AuthClient, + public readonly name: string, + ) {} + + /** + * Creates the role in etcd. + */ + public create(): Promise { + return this.client.roleAdd({ name: this.name }).then(() => this); + } + + /** + * Deletes the role from etcd. + */ + public delete(): Promise { + return this.client.roleDelete({ role: this.name }).then(() => this); + } + + /** + * Removes a permission from the role in etcd. + */ + public revoke(req: IPermissionRequest | IPermissionRequest[]): Promise { + if (req instanceof Array) { + return Promise.all(req.map(r => this.grant(r))).then(() => this); + } + + const range = getRange(req); + return this.client.roleRevokePermission({ + role: this.name, + key: range.start.toString(), + range_end: range.end.toString(), + }) + .then(() => this); + } + + /** + * Grants one or more permissions to this role. + */ + public grant(req: IPermissionRequest | IPermissionRequest[]): Promise { + if (req instanceof Array) { + return Promise.all(req.map(r => this.grant(r))).then(() => this); + } + + const range = getRange(req); + return this.client.roleGrantPermission({ + name: this.name, + perm: { + permType: 'read', + key: range.start, + range_end: range.end, + }, + }) + .then(() => this); + } + + /** + * Returns a list of permissions the role has. + */ + public permissions(): Promise { + return this.client.roleGet({ role: this.name }) + .then(response => { + return response.perm.map(perm => ({ + permission: perm.permType, + range: new Range(perm.key, perm.range_end), + })); + }); + } + + /** + * Grants a user access to the role. + */ + public addUser(user: string | User): Promise { + if (user instanceof User) { + user = user.name; + } + + return this.client.userGrantRole({ user, role: this.name }) + .then(() => this); + } + + /** + * Removes a user's access to the role. + */ + public removeUser(user: string | User): Promise { + if (user instanceof User) { + user = user.name; + } + + return this.client.userRevokeRole({ name: user, role: this.name }) + .then(() => this); + } +} + +/** + * The User provides an entry point for managing etcd users. The user can + * be added to Roles to manage permissions. + */ +export class User { + constructor( + private client: AuthClient, + public readonly name: string, + ) {} + + /** + * Creates the user, with the provided password. + */ + public create(password: string): Promise { + return this.client.userAdd({ name: this.name, password }) + .then(() => this); + } + + /** + * Changes the user's password. + */ + public setPassword(password: string): Promise { + return this.client.userChangePassword({ name: this.name, password }) + .then(() => this); + } + + /** + * Deletes the user from etcd. + */ + public delete(): Promise { + return this.client.userDelete({ name: this.name }).then(() => this); + } + + /** + * Returns a list of roles this user has. + */ + public roles(): Promise { + return this.client.userGet({ name: this.name }).then(res => { + return res.roles.map(role => new Role(this.client, role)); + }); + } + + /** + * Adds the user to a role. + */ + public addRole(role: string | Role): Promise { + if (role instanceof Role) { + role = role.name; + } + + return this.client.userGrantRole({ user: this.name, role }) + .then(() => this); + } + + /** + * Removes the user's access to a role. + */ + public removeRole(role: string | Role): Promise { + if (role instanceof Role) { + role = role.name; + } + + return this.client.userRevokeRole({ name: this.name, role }) + .then(() => this); + } +} diff --git a/src/backoff/backoff.ts b/src/backoff/backoff.ts index 7ee6988..8b5ce59 100644 --- a/src/backoff/backoff.ts +++ b/src/backoff/backoff.ts @@ -1,5 +1,4 @@ export interface IBackoffStrategy { - /** * getDelay returns the amount of delay of the current backoff. */ @@ -15,5 +14,4 @@ export interface IBackoffStrategy { * Returns a strategy with a reset backoff counter. */ reset(): IBackoffStrategy; - } diff --git a/src/backoff/exponential.ts b/src/backoff/exponential.ts index 084da36..d8b1634 100644 --- a/src/backoff/exponential.ts +++ b/src/backoff/exponential.ts @@ -7,7 +7,6 @@ import { IBackoffStrategy } from './backoff'; * given in milliseconds. */ export interface IExponentialOptions { - /** * The initial delay passed to the equation. */ @@ -22,17 +21,15 @@ export interface IExponentialOptions { * max is the maximum value of the delay. */ max: number; - } /** * @see https://en.wikipedia.org/wiki/Exponential_backoff */ export class ExponentialBackoff implements IBackoffStrategy { - private counter: number; - constructor (protected options: IExponentialOptions) { + constructor(protected options: IExponentialOptions) { this.counter = 0; } @@ -59,5 +56,4 @@ export class ExponentialBackoff implements IBackoffStrategy { public reset(): IBackoffStrategy { return new ExponentialBackoff(this.options); } - } diff --git a/src/builder.ts b/src/builder.ts index fbdbdee..e908112 100644 --- a/src/builder.ts +++ b/src/builder.ts @@ -1,59 +1,19 @@ +import { rangable, Range } from './range'; import * as RPC from './rpc'; -import { PromiseWrap } from './util'; +import { PromiseWrap, toBuffer } from './util'; -const zeroKey = Buffer.from([0]); const emptyBuffer = Buffer.from([]); -/** - * prefixStart returns a buffer to start the key as a prefix. - */ -export function prefixStart(key: Buffer | string) { - if (key.length === 0) { - return zeroKey; - } - - return toBuffer(key); -} - -/** - * prefixEnd returns the end of a range request, where `key` is the "start" - * value, to get all values that share the prefix. - */ -export function prefixEnd(key: Buffer): Buffer { - if (key.equals(zeroKey)) { - return zeroKey; - } - - let buffer = Buffer.from(key); // copy to prevent mutation - for (let i = buffer.length - 1; i >= 0; i -= 0) { - if (buffer[i] < 0xff) { - buffer[i] = buffer[i] + 1; - buffer = buffer.slice(0, i + 1); - return buffer; - } - } - - return zeroKey; -} - -export const sortMap = { - key: RPC.SortTarget.KEY, - version: RPC.SortTarget.VERSION, - createdAt: RPC.SortTarget.CREATE, - modifiedAt: RPC.SortTarget.MOD, - value: RPC.SortTarget.VALUE, -}; - /** * Comparators can be passed to various operations in the ComparatorBuilder. */ export const comparator = { - '==': RPC.CompareResult.EQUAL, - '===': RPC.CompareResult.EQUAL, - '>': RPC.CompareResult.GREATER, - '<': RPC.CompareResult.LESS, - '!=': RPC.CompareResult.NOT_EQUAL, - '!==': RPC.CompareResult.NOT_EQUAL, + '==': RPC.CompareResult.equal, + '===': RPC.CompareResult.equal, + '>': RPC.CompareResult.greater, + '<': RPC.CompareResult.less, + '!=': RPC.CompareResult.notEqual, + '!==': RPC.CompareResult.notEqual, }; export interface ICompareTarget { @@ -68,23 +28,11 @@ export interface IOperation { /** * compareTarget are the types of things that can be compared against. */ -export const compareTarget = { - value: { - value: RPC.CompareTarget.VALUE, - key: 'value', - }, - version: { - value: RPC.CompareTarget.VERSION, - key: 'value', - }, - createdAt: { - value: RPC.CompareTarget.CREATE, - key: 'create_revision', - }, - modifiedAt: { - value: RPC.CompareTarget.MOD, - key: 'mod_revision', - }, +export const compareTarget: { [key in keyof typeof RPC.CompareTarget]: keyof RPC.ICompare } = { + value: 'value', + version: 'version', + create: 'create_revision', + mod: 'mod_revision', }; /** @@ -98,17 +46,6 @@ function assertWithin(map: T, value: keyof T, thing: string) { } } -/** - * Converts the input to a buffer, if it is not already. - */ -function toBuffer(input: string | Buffer): Buffer { - if (input instanceof Buffer) { - return input; - } - - return Buffer.from(input); -} - /** * RangeBuilder is a primitive builder for range queries on the kv store. * It's extended by the Single and MultiRangeBuilders, which contain @@ -226,7 +163,6 @@ export class SingleRangeBuilder extends RangeBuilder { * MultiRangeBuilder is a query builder that looks up multiple keys. */ export class MultiRangeBuilder extends RangeBuilder<{ [key: string]: string }> { - private queryPrefix: Buffer; constructor(private kv: RPC.KVClient) { @@ -241,8 +177,16 @@ export class MultiRangeBuilder extends RangeBuilder<{ [key: string]: string }> { */ public prefix(value: string | Buffer): this { this.queryPrefix = toBuffer(value); - this.request.key = prefixStart(value); - this.request.range_end = prefixEnd(this.request.key); + return this.inRange(Range.prefix(value)); + } + + /** + * inRange instructs the builder to get keys in the specified byte range. + */ + public inRange(r: rangable): this { + const range = Range.from(r); + this.request.key = range.start; + this.request.range_end = range.end; return this; } @@ -253,15 +197,6 @@ export class MultiRangeBuilder extends RangeBuilder<{ [key: string]: string }> { return this.prefix(''); } - /** - * inRange instructs the builder to get keys in the specified byte range. - */ - public inRange(start: string | Buffer, end: string | Buffer): this { - this.request.key = toBuffer(start); - this.request.range_end = toBuffer(end); - return this; - } - /** * Limit sets the maximum number of results to retrieve. */ @@ -273,10 +208,11 @@ export class MultiRangeBuilder extends RangeBuilder<{ [key: string]: string }> { /** * Sort specifies how the result should be sorted. */ - public sort(target: keyof typeof sortMap, order: 'asc' | 'desc'): this { - assertWithin(sortMap, target, 'sort order in client.get().sort(...)'); - this.request.sort_target = sortMap[target]; - this.request.sort_order = order.toLowerCase() === 'asc' ? RPC.SortOrder.ASCEND : RPC.SortOrder.DESCEND; + public sort(target: keyof typeof RPC.SortTarget, order: keyof typeof RPC.SortOrder): this { + assertWithin(RPC.SortTarget, target, 'sort order in client.get().sort(...)'); + assertWithin(RPC.SortOrder, order, 'sort order in client.get().sort(...)'); + this.request.sort_target = RPC.SortTarget[target]; + this.request.sort_order = RPC.SortOrder[order]; return this; } @@ -352,7 +288,7 @@ export class MultiRangeBuilder extends RangeBuilder<{ [key: string]: string }> { private mapValues(iterator: (buf: Buffer) => T): Promise<{ [key: string]: T }> { return this.exec().then(res => { const output: { [key: string]: T } = {}; - for (let i = 0; i < res.kvs.length; i += 1) { + for (let i = 0; i < res.kvs.length; i++) { output[res.kvs[i].key.slice(this.queryPrefix.length).toString()] = iterator(res.kvs[i].value); } @@ -382,11 +318,18 @@ export class DeleteBuilder extends PromiseWrap { } /** - * Prefix instructs the query to delete all keys that have the provided prefix. + * key sets a single key to be deleted. */ public prefix(value: string | Buffer): this { - this.request.key = prefixStart(value); - this.request.range_end = prefixEnd(this.request.key); + return this.range(Range.prefix(value)); + } + + /** + * Sets the byte range of values to delete. + */ + public range(range: Range): this { + this.request.key = range.start; + this.request.range_end = range.end; return this; } @@ -400,9 +343,10 @@ export class DeleteBuilder extends PromiseWrap { /** * inRange instructs the builder to delete keys in the specified byte range. */ - public inRange(start: string | Buffer, end: string | Buffer): this { - this.request.key = toBuffer(start); - this.request.range_end = toBuffer(end); + public inRange(r: rangable): this { + const range = Range.from(r); + this.request.key = range.start; + this.request.range_end = range.end; return this; } @@ -523,7 +467,7 @@ export class PutBuilder extends PromiseWrap { * const id = uuid.v4(); * * function lock() { - * return client.if('my_lock', 'createdAt', '==', 0) + * return client.if('my_lock', 'create', '==', 0) * .then(client.put('my_lock').value(id)) * .else(client.get('my_lock')) * .commit() @@ -545,8 +489,12 @@ export class ComparatorBuilder { /** * Adds a new clause to the transaction. */ - public and(key: string | Buffer, column: keyof typeof compareTarget, - cmp: keyof typeof comparator, value: string | Buffer | number): this { + public and( + key: string | Buffer, + column: keyof typeof RPC.CompareTarget, + cmp: keyof typeof comparator, + value: string | Buffer | number, + ): this { assertWithin(compareTarget, column, 'comparison target in client.and(...)'); assertWithin(comparator, cmp, 'comparator in client.and(...)'); @@ -558,8 +506,8 @@ export class ComparatorBuilder { this.request.compare.push({ key: toBuffer(key), result: comparator[cmp], - target: compareTarget[column].value, - [compareTarget[column].key]: typeof value === 'number' ? value : toBuffer(value), + target: RPC.CompareTarget[column], + [compareTarget[column]]: typeof value === 'number' ? value : toBuffer(value), }); return this; } diff --git a/src/connection-pool.ts b/src/connection-pool.ts index 5f08f8f..081bf71 100644 --- a/src/connection-pool.ts +++ b/src/connection-pool.ts @@ -1,11 +1,11 @@ import { ExponentialBackoff } from './backoff/exponential'; import { castGrpcError, GRPCGenericError } from './errors'; import { IOptions } from './options'; -import { ICallable, Services } from './rpc'; +import { ICallable, Services, IAuthenticateResponse } from './rpc'; import { SharedPool } from './shared-pool'; import { forOwn } from './util'; -const grpc = require('grpc'); +const grpc = require('grpc'); // tslint:disable-line const services = grpc.load(`${__dirname}/../proto/rpc.proto`); /** @@ -22,12 +22,101 @@ export const defaultBackoffStrategy = new ExponentialBackoff({ random: 1, }); +/** + * Used for typing internally. + */ +interface GRPCCredentials { + isGRPCCredential: void; +} + +/** + * Retrieves and returns an auth token for accessing etcd. This function is + * based on the algorithm in {@link https://git.io/vHzwh}. + */ +class Authentictor { + private awaitingToken: Promise | null = null; + + constructor(private options: IOptions) {} + + /** + * Augments the call credentials with the configured username and password, + * if any. + */ + public augmentCredentials(original: GRPCCredentials): Promise { + if (this.awaitingToken !== null) { + return this.awaitingToken; + } + + const hosts = typeof this.options.hosts === 'string' + ? [this.options.hosts] + : this.options.hosts; + const auth = this.options.auth; + + if (!auth) { + return Promise.resolve(original); + } + + const attempt = (index: number, previousRejection?: Error): Promise => { + if (index > hosts.length) { + this.awaitingToken = null; + return Promise.reject(previousRejection); + } + + return this.getCredentialsFromHost(hosts[index], auth, original) + .then(token => { + this.awaitingToken = null; + return grpc.credentials.combineChannelCredentials( + original, this.createMetadataAugmenter(token)); + }) + .catch(err => attempt(index + 1, err)); + }; + + return this.awaitingToken = attempt(0); + } + + /** + * Retrieves an auth token from etcd. + */ + private getCredentialsFromHost(address: string, auth: { username: string, password: string}, + credentials: GRPCCredentials): Promise { + + const service = new services.etcdserverpb.Auth(address, credentials); + return new Promise((resolve, reject) => { + service.authenticate( + { name: auth.username, password: auth.password }, + (err: Error | null, res: IAuthenticateResponse) => { + if (err) { + return reject(err); + } + + return resolve(res.token); + } + ); + }); + } + + /** + * Creates a metadata generator that adds the auth token to grpc calls. + */ + private createMetadataAugmenter(token: string): GRPCCredentials { + return grpc.credentials.createFromMetadataGenerator( + (_ctx: any, callback: (err: Error | null, result?: any) => void) => { + const metadata = new grpc.Metadata(); + metadata.add('token', token); + callback(null, metadata); + } + ); + } +} + class Host { - private cachedCredentials: Promise | null = null; private cachedServices: { [name in keyof typeof Services]?: Promise } = Object.create(null); - constructor(private host: string, private options: IOptions) {} + constructor( + private host: string, + private channelCredentials: Promise, + ) {} /** * Returns the given GRPC service on the current host. @@ -38,14 +127,10 @@ class Host { return Promise.resolve(service); } - if (this.cachedCredentials === null) { - this.cachedCredentials = this.buildAuthentication(); - } - - return this.cachedServices[name] = this.cachedCredentials.then(credentials => { - const s = new services.etcdserverpb[name](this.host, credentials); - s.etcdHost = this; - return s; + return this.channelCredentials.then(credentials => { + const instance = new services.etcdserverpb[name](this.host, credentials); + instance.etcdHost = this; + return instance; }); } @@ -54,36 +139,12 @@ class Host { * existing client */ public close() { - if (!this.cachedCredentials) { - return; - } - forOwn(this.cachedServices, (service: Promise) => { service.then(c => grpc.closeClient(c)); }); - this.cachedCredentials = null; this.cachedServices = Object.create(null); } - - private buildAuthentication(): Promise { - const { credentials, auth } = this.options; - - let protocolCredentials = grpc.credentials.createInsecure(); - if (credentials) { - protocolCredentials = grpc.credentials.createSsl( - credentials.rootCertificate, - credentials.privateKey, - credentials.certChain, - ); - } - - if (auth) { - throw new Error('password auth not supported yet'); // todo(connor4312) - } - - return Promise.resolve(grpc.credentials.combineCallCredentials(protocolCredentials)); - } } /** @@ -94,16 +155,10 @@ export class ConnectionPool implements ICallable { private pool = new SharedPool(this.options.backoffStrategy || defaultBackoffStrategy); private mockImpl: ICallable | null; + private authenticator = new Authentictor(this.options); constructor(private options: IOptions) { - if (typeof options.hosts === 'string') { - options.hosts = [options.hosts]; - } - if (options.hosts.length === 0) { - throw new Error('Cannot construct an etcd client with no hosts specified'); - } - - options.hosts.forEach(host => this.pool.add(new Host(host, options))); + this.seedHosts(); } /** @@ -168,4 +223,41 @@ export class ConnectionPool implements ICallable { return this.pool.pull().then(client => client.getService(service)); } + + /** + * Adds configured etcd hosts to the connection pool. + */ + private seedHosts() { + const credentials = this.buildAuthentication(); + const { hosts } = this.options; + + if (typeof hosts === 'string') { + this.pool.add(new Host(hosts, credentials)); + return; + } + + if (hosts.length === 0) { + throw new Error('Cannot construct an etcd client with no hosts specified'); + } + + hosts.forEach(host => this.pool.add(new Host(host, credentials))); + } + + /** + * Creates authentication credentials to use for etcd clients. + */ + private buildAuthentication(): Promise { + const { credentials } = this.options; + + let protocolCredentials = grpc.credentials.createInsecure(); + if (credentials) { + protocolCredentials = grpc.credentials.createSsl( + credentials.rootCertificate, + credentials.privateKey, + credentials.certChain, + ); + } + + return this.authenticator.augmentCredentials(protocolCredentials); + } } diff --git a/src/errors.ts b/src/errors.ts index b39d62b..d95bf4c 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -32,7 +32,7 @@ export class GRPCCancelledError extends GRPCGenericError {} export class EtcdError extends Error {} /** - * EtcdLeaseTimeoutError is thrown when trying to renew a lease that's + * EtcdLeaseInvalidError is thrown when trying to renew a lease that's * expired. */ export class EtcdLeaseInvalidError extends Error { @@ -41,6 +41,34 @@ export class EtcdLeaseInvalidError extends Error { } } +/** + * EtcdRoleExistsError is thrown when trying to create a role that already exists. + */ +export class EtcdRoleExistsError extends Error {} + +/** + * EtcdUserExistsError is thrown when trying to create a user that already exists. + */ +export class EtcdUserExistsError extends Error {} + +/** + * EtcdRoleNotGrantedError is thrown when trying to revoke a role from a user + * to which the role is not granted. + */ +export class EtcdRoleNotGrantedError extends Error {} + +/** + * EtcdRoleNotFoundError is thrown when trying to operate on a role that does + * not exist. + */ +export class EtcdRoleNotFoundError extends Error {} + +/** + * EtcdUserNotFoundError is thrown when trying to operate on a user that does + * not exist. + */ +export class EtcdUserNotFoundError extends Error {} + /** * EtcdLockFailedError is thrown when we fail to aquire a lock. */ @@ -78,6 +106,11 @@ const grpcMessageToError = new Map([ ['Cancelled before creating subchannel', GRPCCancelledError], ['Pick cancelled', GRPCCancelledError], ['Disconnected', GRPCCancelledError], + [/role name already exists/, EtcdRoleExistsError], + [/user name already exists/, EtcdUserExistsError], + [/role is not granted to the user/, EtcdRoleNotGrantedError], + [/role name not found/, EtcdRoleNotFoundError], + [/user name not found/, EtcdUserNotFoundError], ]); function getMatchingGrpcError(err: Error): IErrorCtor | null { @@ -109,9 +142,9 @@ export function castGrpcError(err: Error): Error { return err; // it looks like it's already some kind of typed error } - let ctor: IErrorCtor = getMatchingGrpcError(err) || GRPCGenericError; - if (err.message.includes('etcdserver:')) { - ctor = EtcdError; + let ctor = getMatchingGrpcError(err); + if (!ctor) { + ctor = err.message.includes('etcdserver:') ? EtcdError : GRPCGenericError; } const castError = new ctor(rewriteErrorName(err.message, ctor)); diff --git a/src/index.ts b/src/index.ts index 9a76e75..d9fb43d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,13 +1,18 @@ +import { Role, User } from './auth'; import * as Builder from './builder'; import { ConnectionPool } from './connection-pool'; import { Lease } from './lease'; import { Lock } from './lock'; import { IOptions } from './options'; +import { rangable, Range } from './range'; import * as RPC from './rpc'; -export * from './errors'; +export * from './auth'; export * from './builder'; +export * from './errors'; export * from './lease'; +export * from './options'; +export * from './range'; export * from './rpc'; /** @@ -28,7 +33,6 @@ export * from './rpc'; * ``` */ export class Etcd3 { - private pool = new ConnectionPool(this.options); public readonly kv = new RPC.KVClient(this.pool); @@ -90,11 +94,55 @@ export class Etcd3 { * statements atomically. See documentation on the ComparatorBuilder for * more information. */ - public if(key: string | Buffer, column: keyof typeof Builder.compareTarget, - cmp: keyof typeof Builder.comparator, value: string | Buffer | number): Builder.ComparatorBuilder { + public if( + key: string | Buffer, + column: keyof typeof Builder.compareTarget, + cmp: keyof typeof Builder.comparator, + value: string | Buffer | number, + ): Builder.ComparatorBuilder { return new Builder.ComparatorBuilder(this.kv).and(key, column, cmp, value); } + /** + * Creates a structure representing an etcd range. Used in permission grants + * and queries. This is a convenience method for `Etcd3.Range.from(...)`. + */ + public range(r: rangable): Range { + return Range.from(r); + } + + /** + * Resolves to an array of roles available in etcd. + */ + public getRoles(): Promise { + return this.auth.roleList().then(result => { + return result.roles.map(role => new Role(this.auth, role)); + }); + } + + /** + * Returns an object to manipulate the role with the provided name. + */ + public role(name: string): Role { + return new Role(this.auth, name); + } + + /** + * Resolves to an array of users available in etcd. + */ + public getUsers(): Promise { + return this.auth.userList().then(result => { + return result.users.map(user => new User(this.auth, user)); + }); + } + + /** + * Returns an object to manipulate the user with the provided name. + */ + public user(name: string): User { + return new User(this.auth, name); + } + /** * `.mock()` allows you to insert an interface that will be called into * instead of calling out to the "real" service. `unmock` should be called diff --git a/src/lease.ts b/src/lease.ts index e788893..0c96bae 100644 --- a/src/lease.ts +++ b/src/lease.ts @@ -76,7 +76,6 @@ const enum State { * ``` */ export class Lease extends EventEmitter { - private leaseID: Promise; private state = State.Alive; @@ -263,12 +262,15 @@ export class Lease extends EventEmitter { this.keepalive(); }); - const keepaliveTimer = setInterval(() => { - this.emit('keepaliveFired'); - this.grant() - .then(id => stream.write({ ID: id })) - .catch(() => this.close()); // will only throw if the initial grant failed - }, 1000 * this.ttl / 3); + const keepaliveTimer = setInterval( + () => { + this.emit('keepaliveFired'); + this.grant() + .then(id => stream.write({ ID: id })) + .catch(() => this.close()); // will only throw if the initial grant failed + }, + 1000 * this.ttl / 3, + ); this.teardown = () => { clearInterval(keepaliveTimer); diff --git a/src/lock.ts b/src/lock.ts index dbcd857..62625c2 100644 --- a/src/lock.ts +++ b/src/lock.ts @@ -30,7 +30,6 @@ import * as RPC from './rpc'; * ``` */ export class Lock { - private leaseTTL = 30; private lease: Lease | null; @@ -58,7 +57,7 @@ export class Lock { return lease.grant().then(leaseID => { return new ComparatorBuilder(kv) - .and(this.key, 'createdAt', '==', 0) + .and(this.key, 'create', '==', 0) .then(new PutBuilder(kv, this.key).value('').lease(leaseID)) .commit() .then(res => { diff --git a/src/memoize.ts b/src/memoize.ts new file mode 100644 index 0000000..6d36feb --- /dev/null +++ b/src/memoize.ts @@ -0,0 +1,68 @@ +/** + * Decorates a property or accessor with a memoization. By default it memoizes + * based on a strict equality check of the function's first parameter. Inspired + * by: https://gist.github.com/dsherret/cbe661faf7e3cfad8397 + */ +export function Memoize(hasher: (...args: any[]) => any = value => value) { // tslint:disable-line + return (_target: any, _prop: string, descriptor: TypedPropertyDescriptor) => { + if (descriptor.value != null) { + descriptor.value = getNewFunction(hasher, descriptor.value); + } else if (descriptor.get != null) { + descriptor.get = getNewFunction(hasher, descriptor.get); + } else { + throw new Error('Can only attach @Memoize() to methods and property getters'); + } + }; +} + +const recordsSymbol = Symbol('memoized records'); +const funcIdSymbol = Symbol('unique memoized function id'); + +/** + * Clears memoized values for a function on the provided object instance. + */ +export function forget(instance: any, func: Function) { + const id: string = ( func)[funcIdSymbol]; + if (!id) { + throw new Error('Cannot forget a function that is non memoized!'); + } + if (!instance[recordsSymbol]) { + return; + } + + delete instance[recordsSymbol][id]; +} + +let funcIdCounter = 0; + +/* tslint:disable:no-invalid-this */ +function getNewFunction( + hasher: (...args: any[]) => T, + originalFunction: (...args: any[]) => R, +) { + const id = funcIdCounter; + funcIdCounter++; + + const func = function(this: any) { + let records: { [key: string]: Map } = this[recordsSymbol]; + if (!records) { + records = this[recordsSymbol] = Object.create(null); + } + + let results = records[id]; + if (!results) { + results = records[id] = new Map(); + } + + const hashKey = hasher.apply(this, arguments); + if (results.has(hashKey)) { + return results.get(hashKey); + } + + const result = originalFunction.apply(this, arguments); + results.set(hashKey, result); + return result; + }; + + return Object.assign(func, { [funcIdSymbol]: id }); +} diff --git a/src/range.ts b/src/range.ts new file mode 100644 index 0000000..d8ad025 --- /dev/null +++ b/src/range.ts @@ -0,0 +1,112 @@ +import { toBuffer } from './util'; + +const zeroKey = Buffer.from([0]); +const emptyKey = Buffer.from([]); + +function compare(a: Buffer, b: Buffer) { + if (a.length === 0) { + return b.length === 0 ? 0 : 1; + } + if (b.length === 0) { + return -1; + } + + return a.compare(b); +} + +// rangable is a type that can be converted into an etcd range. +export type rangable = Range + | string + | Buffer + | { start: string | Buffer, end: string | Buffer } + | { prefix: string | Buffer }; + +function rangableIsPrefix(r: rangable): r is { prefix: string | Buffer } { + return r.hasOwnProperty('prefix'); +} + +/** + * Range represents a byte range in etcd. Parts of this class are based on the + * logic found internally within etcd here: + * https://github.com/coreos/etcd/blob/c4a45c57135bf49ae701352c9151dc1be433d1dd/pkg/adt/interval_tree.go + */ +export class Range { + public readonly start: Buffer; + public readonly end: Buffer; + + constructor(start: Buffer | string, end: Buffer | string = emptyKey) { + this.start = toBuffer(start); + this.end = toBuffer(end); + } + + /** + * Returns whether the byte range includes the provided value. + */ + public includes(value: string | Buffer) { + value = toBuffer(value); + return compare(this.start, value) <= 0 && compare(this.end, value) > 0; + } + + /** + * Compares the other range to this one, returning: + * -1 if this range comes before the other one + * 1 if this range comes after the other one + * 0 if they overlap + */ + public compare(other: Range): number { + const ivbCmpBegin = compare(this.start, other.start); + const ivbCmpEnd = compare(this.start, other.end); + const iveCmpBegin = compare(this.end, other.start); + + if (ivbCmpBegin < 0 && iveCmpBegin <= 0) { + return -1; + } + + if (ivbCmpEnd >= 0) { + return 1; + } + + return 0; + } + + /** + * Prefix returns a Range that maps to all keys + * prefixed with the provided string. + */ + public static prefix(prefix: string | Buffer) { + if (prefix.length === 0) { + return new Range(zeroKey, zeroKey); + } + + const start = toBuffer(prefix); + let end = Buffer.from(start); // copy to prevent mutation + for (let i = end.length - 1; i >= 0; i--) { + if (end[i] < 0xff) { + end[i]++; + end = end.slice(0, i + 1); + return new Range(start, end); + } + } + + return new Range(start, zeroKey); + } + + /** + * Converts a rangable into a qualified Range. + */ + public static from(v: rangable): Range { + if (typeof v === 'string' || v instanceof Buffer) { + return new Range(toBuffer(v)); + } + + if (v instanceof Range) { + return v; + } + + if (rangableIsPrefix(v)) { + return Range.prefix(v.prefix); + } + + return new Range(v.start, v.end); + } +} diff --git a/src/rpc.ts b/src/rpc.ts index ae6a034..6ea304e 100644 --- a/src/rpc.ts +++ b/src/rpc.ts @@ -1,7 +1,3 @@ -/** - * RPC module doc here - */ - // AUTOGENERATED CODE, DO NOT EDIT // tslint:disable @@ -297,22 +293,22 @@ export enum SortOrder { /** * default, no sorting */ - NONE = 0, + none = 0, /** * lowest target value first */ - ASCEND = 1, + ascend = 1, /** * highest target value first */ - DESCEND = 2, + descend = 2, } export enum SortTarget { - KEY = 0, - VERSION = 1, - CREATE = 2, - MOD = 3, - VALUE = 4, + key = 0, + version = 1, + create = 2, + mod = 3, + value = 4, } export interface IRangeRequest { /** @@ -341,11 +337,11 @@ export interface IRangeRequest { /** * sort_order is the order for returned sorted results. */ - sort_order?: SortOrder; + sort_order?: SortOrder | keyof typeof SortOrder; /** * sort_target is the key-value field to use for sorting. */ - sort_target?: SortTarget; + sort_target?: SortTarget | keyof typeof SortTarget; /** * serializable sets the range request to use serializable member-local reads. * Range requests are linearizable by default; linearizable requests have higher @@ -478,26 +474,26 @@ export interface IResponseOp { response_delete_range: IDeleteRangeResponse; } export enum CompareResult { - EQUAL = 0, - GREATER = 1, - LESS = 2, - NOT_EQUAL = 3, + equal = 0, + greater = 1, + less = 2, + notEqual = 3, } export enum CompareTarget { - VERSION = 0, - CREATE = 1, - MOD = 2, - VALUE = 3, + version = 0, + create = 1, + mod = 2, + value = 3, } export interface ICompare { /** * result is logical comparison operation for this comparison. */ - result?: CompareResult; + result?: CompareResult | keyof typeof CompareResult; /** * target is the key-value field to inspect for the comparison. */ - target?: CompareTarget; + target?: CompareTarget | keyof typeof CompareTarget; /** * key is the subject key for the comparison operation. */ @@ -551,7 +547,7 @@ export interface ITxnResponse { } export interface ICompactionRequest { /** - * revision is the key-value store revision for the compaction operation. + * revision is the key-value store revision for the compaction operation. */ revision?: string | number; /** @@ -594,11 +590,11 @@ export enum FilterType { /** * filter out put event. */ - NOPUT = 0, + noput = 0, /** * filter out delete event. */ - NODELETE = 1, + nodelete = 1, } export interface IWatchCreateRequest { /** @@ -627,7 +623,7 @@ export interface IWatchCreateRequest { /** * filters filter the events at server side before it sends back to the watcher. */ - filters?: FilterType[]; + filters?: FilterType | keyof typeof FilterType[]; /** * If prev_kv is set, created watcher gets the previous KV before the event happens. * If the previous KV is already compacted, nothing will be returned. @@ -768,6 +764,10 @@ export interface IMemberAddResponse { * member is the member information for the added member. */ member: IMember; + /** + * members is a list of all members after adding the new member. + */ + members: IMember[]; } export interface IMemberRemoveRequest { /** @@ -777,6 +777,10 @@ export interface IMemberRemoveRequest { } export interface IMemberRemoveResponse { header: IResponseHeader; + /** + * members is a list of all members after removing the member. + */ + members: IMember[]; } export interface IMemberUpdateRequest { /** @@ -790,6 +794,10 @@ export interface IMemberUpdateRequest { } export interface IMemberUpdateResponse { header: IResponseHeader; + /** + * members is a list of all members after updating the member. + */ + members: IMember[]; } export interface IMemberListResponse { header: IResponseHeader; @@ -805,16 +813,16 @@ export enum AlarmType { /** * default, used to query if any alarm is active */ - NONE = 0, + none = 0, /** * space quota is exhausted */ - NOSPACE = 1, + nospace = 1, } export enum AlarmAction { - GET = 0, - ACTIVATE = 1, - DEACTIVATE = 2, + get = 0, + activate = 1, + deactivate = 2, } export interface IAlarmRequest { /** @@ -822,7 +830,7 @@ export interface IAlarmRequest { * may GET alarm statuses, ACTIVATE an alarm, or DEACTIVATE a * raised alarm. */ - action?: AlarmAction; + action?: AlarmAction | keyof typeof AlarmAction; /** * memberID is the ID of the member associated with the alarm. If memberID is 0, the * alarm request covers all members. @@ -831,7 +839,7 @@ export interface IAlarmRequest { /** * alarm is the type of alarm to consider for this request. */ - alarm?: AlarmType; + alarm?: AlarmType | keyof typeof AlarmType; } export interface IAlarmMember { /** @@ -841,7 +849,7 @@ export interface IAlarmMember { /** * alarm is the type of alarm which has been raised. */ - alarm: AlarmType; + alarm: keyof typeof AlarmType; } export interface IAlarmResponse { header: IResponseHeader; @@ -997,25 +1005,6 @@ export interface IAuthRoleGrantPermissionResponse { export interface IAuthRoleRevokePermissionResponse { header: IResponseHeader; } -export interface IUser { - name?: Buffer; - password?: Buffer; - roles?: string[]; -} -export enum Type { - READ = 0, - WRITE = 1, - READWRITE = 2, -} -export interface IPermission { - permType: Type; - key: Buffer; - range_end: Buffer; -} -export interface IRole { - name?: Buffer; - keyPermission?: IPermission[]; -} export interface IKeyValue { /** * key is the first key for the range. If range_end is not given, the request only looks up key. @@ -1042,14 +1031,14 @@ export enum EventType { /** * filter out put event. */ - PUT = 0, + put = 0, /** * filter out delete event. */ - DELETE = 1, + delete = 1, } export interface IEvent { - type: EventType; + type: keyof typeof EventType; /** * if prev_kv is set in the request, the previous key-value pair will be returned. */ @@ -1060,6 +1049,25 @@ export interface IEvent { */ prev_kv: IKeyValue; } +export interface IUser { + name?: Buffer; + password?: Buffer; + roles?: string[]; +} +export enum Permission { + read = 0, + write = 1, + readwrite = 2, +} +export interface IPermission { + permType: keyof typeof Permission; + key: Buffer; + range_end: Buffer; +} +export interface IRole { + name?: Buffer; + keyPermission?: IPermission[]; +} export const Services = { KV: KVClient, Watch: WatchClient, diff --git a/src/shared-pool.ts b/src/shared-pool.ts index 2d2c8b2..2ec12fe 100644 --- a/src/shared-pool.ts +++ b/src/shared-pool.ts @@ -58,10 +58,10 @@ export class SharedPool { } const nextAvailable = minBy(available, r => r.availableAfter); - this.contentionCount += 1; + this.contentionCount++; return delay(nextAvailable[0].availableAfter - now).then(() => { - this.contentionCount -= 1; + this.contentionCount--; return this.pull(); }); } diff --git a/src/util.ts b/src/util.ts index 8751ab2..bec80aa 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,3 +1,13 @@ +/** + * Converts the input to a buffer, if it is not already. + */ +export function toBuffer(input: string | Buffer): Buffer { + if (input instanceof Buffer) { + return input; + } + + return Buffer.from(input); +} /** * Returns items with the smallest value as picked by the `prop` function. @@ -5,7 +15,7 @@ export function minBy(items: T[], prop: (x: T) => number): T[] { let min = prop(items[0]); let output = [items[0]]; - for (let i = 1; i < items.length; i += 1) { + for (let i = 1; i < items.length; i++) { const thisMin = prop(items[i]); if (thisMin < min) { min = thisMin; @@ -37,7 +47,7 @@ export function delay(duration: number): Promise { */ export function forOwn(obj: T, iterator: (value: T[K], key: K) => void): void { const keys = <(keyof T)[]> Object.keys(obj); - for (let i = 0; i < keys.length; i += 1) { + for (let i = 0; i < keys.length; i++) { iterator(obj[keys[i]], keys[i]); } } @@ -47,7 +57,6 @@ export function forOwn(obj: T, iterator: (value: T[K], key * method when called. */ export abstract class PromiseWrap implements PromiseLike { - /** * createPromise should ben override to run the promised action. */ diff --git a/test/_setup.ts b/test/_setup.ts index 11bc3ac..62bfda8 100644 --- a/test/_setup.ts +++ b/test/_setup.ts @@ -2,6 +2,6 @@ import * as chai from 'chai'; import { SharedPool } from '../src/shared-pool'; -chai.use(require('chai-subset')); +chai.use(require('chai-subset')); // tslint:disable-line ( SharedPool).deterministicInsertion = true; diff --git a/test/backoff.test.ts b/test/backoff.test.ts index 6dddcbf..c11cbe8 100644 --- a/test/backoff.test.ts +++ b/test/backoff.test.ts @@ -12,7 +12,7 @@ describe('backoff strategies', () => { random: 1, }); - function next () { + function next() { const value = exp.getDelay(); exp = exp.next(); return value; diff --git a/test/certs/certs/ca.crt b/test/certs/certs/ca.crt new file mode 100644 index 0000000..585f492 --- /dev/null +++ b/test/certs/certs/ca.crt @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIFNTCCAx2gAwIBAgIJALtYSoK0DRBnMA0GCSqGSIb3DQEBCwUAMDsxCzAJBgNV +BAYTAlVTMRowGAYDVQQDDBFjYS5ldGNkLmxvY2FsaG9zdDEQMA4GA1UECgwHZXRj +ZC1jYTAeFw0xNzA1MzExNTE4NTNaFw0xNzA2MzAxNTE4NTNaMDsxCzAJBgNVBAYT +AlVTMRowGAYDVQQDDBFjYS5ldGNkLmxvY2FsaG9zdDEQMA4GA1UECgwHZXRjZC1j +YTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMZZ4qqeh58ida0Blad6 +JVhC2WrGPCcdgcLNC49UPODu8hLtMGEVrdJWoxodDsiBYnNLvZUNtTqkp/PMOkKY +LMBCHY6TvZ1rqBy5uNebMzrpNb1gSjc4NlJIIzyeUCj3637SUgBq2k8MsynD8x4j +9DsK7SYivXHgiP94vFgHU/OF6RnfifH4McQvCmJUWBNrODKGBk6WtLQXZ2Y20JXH +J1CSzCiEdnY4jH3XJ+hVLJH517a37pWzRTH7llwFAkeDwOVlG5aJnFDXbedogJDc +t6khg0jmeYLczzeio5+xrJpaA3ewEFRtT2UJwVFtrz8LQFsbU8fml0tdYhfgRqhV +LMyCDqAMGkav6kRSn2yY+bior3HTO2DvnANU/gDufTdPYEU/0C/5B2/PvR5yJjYg +kC6f1r8xnzqwoIULDyktehK0kB8Pqi7B0G5/r9dN77N0ar9SSzrD6XR4dj7F1OjY +u/LyiA+l8Fis9GjI4punIdsuhlNkpbQ1mfZES49gE5PC+cQK7bPEzNLB+4qCa2Sb +BNMiZrMYHwY1hn4CuALusWE2oqNBds1N3S3M9TavSBZmVXgdDYSfTmtB2e+MBOZ/ +OXmzTqikd3qNjUbj4Y/pXo6eBZMiigaz4ZHmmjrZ9lzb1B8Yf54IudbQvG/b6mF9 +c1Wsc2oi+u6a2HHZ+hHSuQApAgMBAAGjPDA6MAwGA1UdEwQFMAMBAf8wCwYDVR0P +BAQDAgEGMB0GA1UdDgQWBBRCY0WEctaWd+f8zK65NPjjmLu9gTANBgkqhkiG9w0B +AQsFAAOCAgEAhN9xjw7Tpm0PqhysYoxP34EQgrx2IiNf7V+/rWlanGSW1Y0vm5Yn +uVJZTM4Lm7ZkS8MNObJn1pVeGvFFJ1xeeuRM0JlUNh0qz89IysCEXxkNnfxWc7qc +ZVU6EAXfBKbu8uvnhdFW8xtDbRVt1iuzzjPx5XE/qtoIdWPHp4QBYdXoFFF9KhSY +xG+GXrOWY6FMTc2o741XENIyn/Ow+xwlkx7o/PiM26t1uCdagrnWrwMh0x77qIkH +NgM5CcG9iziNkiEpwC89Ly6/dkSiAA5saKfjjAKXWFd11TcOn54p63fctKoNqhVa +FCm6yVeWiutgsmnx/VdfnZMmmjnVC1CcGTzaFoTFtI08xML0Z7+YhaaxBQTnwrdQ +ie7+EYQNYQhtfDmsxUl354jaM2IACK0K8HUqYTeHVZVgFTIxXIUB3v5weMui+CAd +AAm9qQZPvlTqaymR6fU/vHeNfWYSoTiXprUv0iBUoGLht5vhIECYG+LCcqnBkuvC +hBWNIUs3TBuCz0Q7lwcslMB3pMQdi+53RYKGDQiLSoWnoBKgIRd9zCq22JrzrhEl +zH9uOfMLUbHpIo0c73U4MSWgIoz0IaWnBC3HkzK/km5NU+XR48Oso55xD9agg29p +gey8l0Jkrt0vRq9CIIASVV1ZO8cpHV/aWx1GNQlhxChrDiPQdndxzT8= +-----END CERTIFICATE----- diff --git a/test/certs/certs/etcd-client.crt b/test/certs/certs/etcd-client.crt new file mode 100644 index 0000000..83b3154 --- /dev/null +++ b/test/certs/certs/etcd-client.crt @@ -0,0 +1,117 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 2 (0x2) + Signature Algorithm: sha512WithRSAEncryption + Issuer: C=US, CN=ca.etcd.localhost, O=etcd-ca + Validity + Not Before: May 31 15:21:47 2017 GMT + Not After : May 29 15:21:47 2027 GMT + Subject: O=etcd-ca, CN=etcd-client + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (4096 bit) + Modulus: + 00:9c:b7:61:32:f0:d0:4f:76:bd:e9:61:8e:cb:f0: + 64:a4:48:e1:7d:1d:b8:53:b3:47:56:ae:21:6b:13: + 1c:90:dd:62:9f:c6:44:d7:1a:ab:5f:1f:45:28:63: + 57:ff:60:c8:c7:9e:73:e4:a8:08:ea:05:b8:3e:92: + ec:c6:90:07:56:d2:7f:33:7b:ba:ea:e3:e8:fb:11: + db:7f:c1:20:cd:23:5b:bf:44:50:a2:3e:59:5e:14: + 52:6c:27:d1:bd:6c:3c:d4:14:4a:7a:ca:d4:7e:38: + 98:8d:85:1b:e2:7b:89:c0:1c:61:c2:e4:70:23:9d: + f1:72:02:ce:59:3b:65:66:6d:04:54:32:0f:6d:ea: + 5a:0d:68:7b:40:bb:a2:95:0d:a7:60:c2:fc:33:5f: + 26:66:3e:c9:3f:76:b2:5a:0a:4d:8f:a2:e3:c5:72: + e3:ef:5e:06:fc:38:2d:87:12:de:2b:62:91:1b:b4: + 45:be:c3:8f:cc:21:36:f1:6f:a3:b6:4d:c0:18:09: + 17:0f:6d:d3:bd:a9:25:8d:64:9b:a8:ca:c0:bc:be: + 96:a4:51:df:a0:48:c1:07:87:b6:c5:5a:3b:8c:6f: + 9c:b0:ac:97:d2:41:05:7c:1d:bf:7c:f7:fa:26:ab: + ac:d6:54:60:4e:21:41:2a:aa:47:84:cb:b3:3d:e2: + 01:c3:3f:7a:32:26:e6:ca:cb:c3:fc:2e:e5:63:4f: + 9f:47:44:a7:a5:b1:55:23:ce:9d:75:a1:08:02:f7: + db:f2:63:5e:3a:c2:35:f0:ad:a9:a7:88:e2:95:7a: + 2d:c0:0e:19:be:83:79:bf:48:3b:51:5b:e7:67:6b: + 45:db:69:ab:2e:23:f4:86:1f:2b:e6:c0:90:dc:e6: + 16:8f:82:bc:43:ef:c3:76:23:68:d9:1e:99:37:6f: + 40:37:d0:b2:49:d3:da:1f:49:3a:c3:f8:19:e1:66: + a7:9a:22:56:c6:ce:f7:85:df:37:f7:f4:2f:74:55: + 66:f3:a9:f5:11:bb:67:97:3b:31:4c:fb:e7:67:1c: + 90:6b:f2:2f:56:bb:6d:cf:28:e0:bb:35:87:ad:dc: + 69:02:e3:79:24:38:f2:75:cf:52:81:c1:42:c8:f4: + 4e:08:b4:7c:f5:fc:f4:1a:5a:e4:6f:74:65:80:2a: + 12:ad:50:73:62:08:23:6c:30:a1:89:c6:92:bf:1b: + 6c:22:8f:38:f3:d2:9e:c5:6d:7a:82:91:63:3d:ec: + 58:ee:d5:cf:97:eb:03:6d:14:fc:42:3a:a4:df:7a: + f4:c6:c1:37:aa:37:98:e0:46:c2:99:e4:5f:7b:f3: + 1f:ec:52:26:8a:d5:f1:b8:97:d2:0b:17:b0:aa:d2: + b5:8a:a7 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + X509v3 Extended Key Usage: + TLS Web Client Authentication + X509v3 Key Usage: + Digital Signature, Key Encipherment + Signature Algorithm: sha512WithRSAEncryption + b6:a5:6c:5b:8c:a4:dc:5c:a5:43:f0:c0:47:49:15:c2:81:d9: + 58:b7:53:41:f1:cc:e8:14:f2:2e:76:59:2d:d7:08:3e:e1:72: + 52:0a:30:79:fc:76:a3:ee:a6:d1:07:7d:bc:4b:97:62:5b:e0: + 69:62:7e:4d:0e:f7:2b:be:11:c2:56:3d:31:89:12:c1:0d:34: + 82:87:e8:d6:aa:92:d4:5c:d6:7c:b1:46:4d:87:de:7d:20:12: + 7f:35:76:f6:27:ef:0c:84:c3:e9:95:dd:c6:07:7e:d5:4d:5d: + c2:e7:63:5b:cd:59:59:ec:87:32:64:10:19:9b:8d:e1:b1:8a: + 28:a3:0a:ba:30:8c:9d:15:63:ea:a2:42:90:34:7b:a7:aa:9b: + 95:b2:5e:a6:0d:50:29:67:8f:2e:c6:f9:8d:0b:3a:e3:f5:11: + 27:d3:92:97:5a:62:f8:b2:ef:c4:b2:2a:c7:32:c6:13:bf:c8: + 89:76:19:43:bb:d0:52:0c:14:af:f2:67:9d:f6:32:2d:2f:18: + e0:ac:5f:d0:e6:89:43:0b:06:8f:f0:33:78:86:cb:e6:07:64: + 62:7d:89:ca:06:58:23:a8:9b:39:2a:38:9f:18:93:f0:8b:d2: + bb:5f:b8:f7:55:46:1a:01:bf:c1:ea:b5:9c:01:72:65:ca:fa: + ec:63:ef:58:59:d0:7c:08:cf:25:3d:45:fb:4e:8d:51:7d:83: + 74:31:2f:0a:a1:38:02:c6:a3:77:6f:c9:10:87:72:14:e9:cb: + 89:82:e1:ce:3f:86:4e:66:a1:ca:40:bd:3a:27:81:a4:75:6c: + 25:8e:f0:16:b8:9c:45:3a:26:b4:06:53:24:e4:f9:d5:5f:1d: + 8e:55:23:cb:44:1f:90:dc:5e:74:b1:c3:4e:6d:8a:60:de:9a: + 45:16:8c:05:66:c4:84:9e:76:d9:d8:56:25:12:ff:9b:fc:97: + 4d:05:32:8a:9a:5e:ff:a4:e2:b0:64:1a:31:4b:b1:37:4a:fe: + 6b:a3:c5:49:ff:75:bf:1b:d0:81:16:70:a5:5a:af:75:ed:ed: + ed:95:15:57:68:5d:36:57:06:b5:81:15:6a:9f:03:78:5c:8e: + 9f:51:89:27:de:a6:ad:f3:5b:11:0a:04:be:7d:ca:a1:c5:f2: + 1a:c0:63:3c:67:f8:b7:f6:05:98:9e:b7:64:69:e8:15:69:fa: + 05:5f:20:dd:be:a2:bb:c0:8b:c4:f8:a9:96:04:6b:86:a5:18: + 43:eb:6d:1f:b9:d8:76:46:e2:df:a5:0e:bc:d8:43:62:9f:6f: + bc:77:2b:30:50:a1:de:4d:cd:58:a9:4b:49:9f:ee:88:ff:be: + 73:ff:e1:c0:f5:04:af:48 +-----BEGIN CERTIFICATE----- +MIIFDTCCAvWgAwIBAgIBAjANBgkqhkiG9w0BAQ0FADA7MQswCQYDVQQGEwJVUzEa +MBgGA1UEAwwRY2EuZXRjZC5sb2NhbGhvc3QxEDAOBgNVBAoMB2V0Y2QtY2EwHhcN +MTcwNTMxMTUyMTQ3WhcNMjcwNTI5MTUyMTQ3WjAoMRAwDgYDVQQKDAdldGNkLWNh +MRQwEgYDVQQDDAtldGNkLWNsaWVudDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC +AgoCggIBAJy3YTLw0E92velhjsvwZKRI4X0duFOzR1auIWsTHJDdYp/GRNcaq18f +RShjV/9gyMeec+SoCOoFuD6S7MaQB1bSfzN7uurj6PsR23/BIM0jW79EUKI+WV4U +Umwn0b1sPNQUSnrK1H44mI2FG+J7icAcYcLkcCOd8XICzlk7ZWZtBFQyD23qWg1o +e0C7opUNp2DC/DNfJmY+yT92sloKTY+i48Vy4+9eBvw4LYcS3itikRu0Rb7Dj8wh +NvFvo7ZNwBgJFw9t072pJY1km6jKwLy+lqRR36BIwQeHtsVaO4xvnLCsl9JBBXwd +v3z3+iarrNZUYE4hQSqqR4TLsz3iAcM/ejIm5srLw/wu5WNPn0dEp6WxVSPOnXWh +CAL32/JjXjrCNfCtqaeI4pV6LcAOGb6Deb9IO1Fb52drRdtpqy4j9IYfK+bAkNzm +Fo+CvEPvw3YjaNkemTdvQDfQsknT2h9JOsP4GeFmp5oiVsbO94XfN/f0L3RVZvOp +9RG7Z5c7MUz752cckGvyL1a7bc8o4Ls1h63caQLjeSQ48nXPUoHBQsj0Tgi0fPX8 +9Bpa5G90ZYAqEq1Qc2III2wwoYnGkr8bbCKPOPPSnsVteoKRYz3sWO7Vz5frA20U +/EI6pN969MbBN6o3mOBGwpnkX3vzH+xSJorV8biX0gsXsKrStYqnAgMBAAGjLzAt +MAkGA1UdEwQCMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwCwYDVR0PBAQDAgWgMA0G +CSqGSIb3DQEBDQUAA4ICAQC2pWxbjKTcXKVD8MBHSRXCgdlYt1NB8czoFPIudlkt +1wg+4XJSCjB5/Haj7qbRB328S5diW+BpYn5NDvcrvhHCVj0xiRLBDTSCh+jWqpLU +XNZ8sUZNh959IBJ/NXb2J+8MhMPpld3GB37VTV3C52NbzVlZ7IcyZBAZm43hsYoo +owq6MIydFWPqokKQNHunqpuVsl6mDVApZ48uxvmNCzrj9REn05KXWmL4su/EsirH +MsYTv8iJdhlDu9BSDBSv8med9jItLxjgrF/Q5olDCwaP8DN4hsvmB2RifYnKBlgj +qJs5KjifGJPwi9K7X7j3VUYaAb/B6rWcAXJlyvrsY+9YWdB8CM8lPUX7To1RfYN0 +MS8KoTgCxqN3b8kQh3IU6cuJguHOP4ZOZqHKQL06J4GkdWwljvAWuJxFOia0BlMk +5PnVXx2OVSPLRB+Q3F50scNObYpg3ppFFowFZsSEnnbZ2FYlEv+b/JdNBTKKml7/ +pOKwZBoxS7E3Sv5ro8VJ/3W/G9CBFnClWq917e3tlRVXaF02Vwa1gRVqnwN4XI6f +UYkn3qat81sRCgS+fcqhxfIawGM8Z/i39gWYnrdkaegVafoFXyDdvqK7wIvE+KmW +BGuGpRhD620fudh2RuLfpQ682ENin2+8dyswUKHeTc1YqUtJn+6I/75z/+HA9QSv +SA== +-----END CERTIFICATE----- diff --git a/test/certs/certs/etcd0.localhost.crt b/test/certs/certs/etcd0.localhost.crt new file mode 100644 index 0000000..ad3aaa5 --- /dev/null +++ b/test/certs/certs/etcd0.localhost.crt @@ -0,0 +1,119 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 1 (0x1) + Signature Algorithm: sha512WithRSAEncryption + Issuer: C=US, CN=ca.etcd.localhost, O=etcd-ca + Validity + Not Before: May 31 15:20:32 2017 GMT + Not After : May 29 15:20:32 2027 GMT + Subject: O=etcd-ca, CN=etcd0.localhost + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (4096 bit) + Modulus: + 00:da:c3:56:43:3f:7d:50:48:dd:9b:16:4f:c1:05: + db:ad:9d:ae:8f:4e:aa:0b:f4:d8:04:a1:39:27:dc: + dc:75:05:e7:88:b6:b2:e0:b4:3a:cc:fe:19:b3:5b: + 3b:ea:74:63:2c:70:58:c8:f4:42:98:ec:5e:78:c8: + 36:21:15:38:0a:2d:84:61:93:56:5a:2b:e9:ca:61: + a5:fe:c9:a2:95:8f:69:af:14:2b:24:fa:1a:05:24: + 23:57:a1:9a:7c:09:f4:6d:79:1d:17:b4:01:5e:07: + ca:2c:c4:9a:bb:a5:c1:37:12:92:8d:51:06:1a:5c: + c2:73:56:d4:7e:be:a9:59:80:3a:11:0b:b4:13:f9: + 64:d3:d3:86:9c:58:a0:e2:0e:67:76:d9:28:37:63: + 9b:89:73:60:43:d6:50:b3:c7:39:2f:4e:85:82:df: + a1:b0:87:9d:5a:70:b8:15:0c:e7:63:55:73:26:4d: + 5e:bb:34:24:23:9e:ab:3b:7e:8c:71:aa:98:6a:41: + 2c:1d:ce:0f:7c:fd:fd:b1:c8:c0:28:0d:d1:8e:63: + 90:ed:c7:0e:1b:5f:9b:c9:80:6c:72:ee:9c:67:f4: + e8:d8:27:75:88:78:1a:ae:90:85:49:f8:e7:aa:ce: + c2:57:cb:1f:fb:a6:ea:8b:c2:e7:e2:13:f4:c7:b9: + 0a:d0:af:3e:94:87:c0:37:b5:8f:d5:b9:6a:1a:27: + 90:e7:a6:7e:06:98:5e:2f:ec:9d:ab:1d:e6:22:bd: + be:a1:82:db:22:5f:f5:ec:ee:a4:bf:e8:89:6e:ea: + da:6f:a9:88:e8:68:76:e3:41:19:88:32:fa:16:65: + 41:a8:ab:5c:6c:6c:84:26:71:10:33:68:8a:9f:3b: + fe:b5:7c:36:2d:18:a0:2d:ac:92:18:ad:4d:6c:ac: + 15:d0:77:7a:ad:09:86:3f:b4:ac:0c:1c:09:83:9a: + 59:37:25:2d:0b:d1:44:04:3b:d9:3e:3c:fb:48:de: + 1e:e8:e0:77:ab:6d:f6:4b:41:bd:72:65:63:80:d3: + be:a5:d3:c5:00:25:5f:7d:29:fc:86:dc:fc:8e:16: + 92:cc:7a:d7:cb:e2:42:37:62:b5:12:91:69:71:56: + 84:7a:e4:ca:43:77:b0:05:5e:b5:a4:35:9d:83:a5: + f3:24:46:bf:49:85:64:1f:63:81:7b:8c:4a:44:db: + 0b:eb:d8:81:d4:14:67:3e:a6:34:b9:4a:55:b6:39: + d9:4b:c3:fc:9e:b4:bf:39:fe:ae:d2:ab:f2:09:a9: + 67:83:fd:6b:b4:32:e9:9e:b8:d5:93:72:3d:b9:e8: + d0:8b:eb:c3:3e:7a:b2:25:ab:ee:b8:a5:ab:4a:47: + f7:77:a9 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + X509v3 Extended Key Usage: + TLS Web Client Authentication, TLS Web Server Authentication + X509v3 Key Usage: + Digital Signature, Key Encipherment + X509v3 Subject Alternative Name: + IP Address:127.0.0.1, IP Address:10.0.1.10 + Signature Algorithm: sha512WithRSAEncryption + 24:8d:61:d0:08:64:f5:1c:30:5f:19:44:1a:4b:0f:1e:a3:85: + ad:6b:0a:1c:4c:3d:89:6f:cb:86:f0:a6:59:72:7d:91:33:68: + 6a:90:f0:10:27:6b:54:fe:c9:94:b9:8a:ec:c5:25:61:0b:9f: + c9:7f:41:14:a4:dd:0d:72:f9:ed:31:e5:b3:d8:a7:f0:82:00: + 58:20:2a:96:53:2e:f6:73:7e:77:ae:1d:19:3b:5b:fc:b3:90: + 39:f0:6d:82:60:66:a3:c4:98:ba:c8:92:7e:e1:4a:a5:79:99: + 2c:60:79:cc:86:ec:e5:e7:d5:fb:b8:78:0c:dd:7a:2d:a4:2f: + 25:ff:b4:ff:48:68:f1:60:c9:0f:93:94:df:a5:1e:1e:1a:0c: + 63:67:ae:5a:15:23:77:94:5c:b4:75:b7:06:30:62:45:e7:5e: + 8b:c1:77:a4:22:8b:fc:de:49:95:e6:a8:df:2a:25:05:3d:7b: + 24:39:af:5f:94:12:a1:29:50:23:03:8d:f5:5e:d1:20:e7:37: + bc:83:02:de:30:51:54:58:48:41:ef:d4:6e:90:6b:68:87:6c: + d8:0a:3a:5f:62:b5:5f:35:42:b6:5e:39:0e:9c:19:0e:3f:e1: + 68:f6:dc:91:a7:74:0b:5f:7f:81:78:55:85:a5:9a:28:7b:64: + 19:e8:54:ba:77:fc:62:e3:1f:e2:28:09:4d:28:08:98:6e:f9: + b3:b8:90:32:4c:69:a5:a5:72:49:88:03:bd:45:36:15:a8:66: + dc:b5:98:7d:6e:80:7c:5c:f2:dd:ff:62:91:b7:90:c7:9d:ce: + 9a:da:e1:62:c8:07:2a:7b:d5:31:b5:c4:0f:44:59:1a:58:0b: + 7a:35:6d:1b:e7:d6:15:5b:1d:48:dd:8e:f7:13:91:ff:4d:32: + 1a:06:7c:01:38:dd:eb:9d:a8:63:3c:60:33:29:7d:e4:b4:08: + e8:12:1b:da:ee:3c:28:eb:e5:b5:c6:51:41:8d:82:7f:32:c9: + 50:f5:41:a2:15:b1:64:e1:ec:01:eb:b2:2e:4b:60:57:32:79: + 36:12:e7:ca:6c:28:8d:92:5f:74:10:f3:b2:8d:76:9b:ec:f6: + 9e:34:81:2a:62:49:a0:ca:13:4d:9f:92:f6:7c:12:dc:76:fb: + 11:cc:ae:e6:f2:6a:27:2b:7d:04:c9:91:ce:2f:d8:ab:6b:f0: + 4e:e3:b4:63:e1:38:c8:fa:e3:41:43:9a:2c:8c:31:13:b8:cc: + ec:e2:24:6e:90:67:da:a4:59:68:ad:f6:6e:7c:fd:d8:f5:f1: + 8c:e3:5d:80:aa:21:d9:14:ca:9b:cd:25:bb:d2:19:1c:f5:04: + ef:2e:b7:42:59:35:2d:df +-----BEGIN CERTIFICATE----- +MIIFMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQ0FADA7MQswCQYDVQQGEwJVUzEa +MBgGA1UEAwwRY2EuZXRjZC5sb2NhbGhvc3QxEDAOBgNVBAoMB2V0Y2QtY2EwHhcN +MTcwNTMxMTUyMDMyWhcNMjcwNTI5MTUyMDMyWjAsMRAwDgYDVQQKDAdldGNkLWNh +MRgwFgYDVQQDDA9ldGNkMC5sb2NhbGhvc3QwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQDaw1ZDP31QSN2bFk/BBdutna6PTqoL9NgEoTkn3Nx1BeeItrLg +tDrM/hmzWzvqdGMscFjI9EKY7F54yDYhFTgKLYRhk1ZaK+nKYaX+yaKVj2mvFCsk ++hoFJCNXoZp8CfRteR0XtAFeB8osxJq7pcE3EpKNUQYaXMJzVtR+vqlZgDoRC7QT ++WTT04acWKDiDmd22Sg3Y5uJc2BD1lCzxzkvToWC36Gwh51acLgVDOdjVXMmTV67 +NCQjnqs7foxxqphqQSwdzg98/f2xyMAoDdGOY5Dtxw4bX5vJgGxy7pxn9OjYJ3WI +eBqukIVJ+OeqzsJXyx/7puqLwufiE/THuQrQrz6Uh8A3tY/VuWoaJ5Dnpn4GmF4v +7J2rHeYivb6hgtsiX/Xs7qS/6Ilu6tpvqYjoaHbjQRmIMvoWZUGoq1xsbIQmcRAz +aIqfO/61fDYtGKAtrJIYrU1srBXQd3qtCYY/tKwMHAmDmlk3JS0L0UQEO9k+PPtI +3h7o4HerbfZLQb1yZWOA076l08UAJV99KfyG3PyOFpLMetfL4kI3YrUSkWlxVoR6 +5MpDd7AFXrWkNZ2DpfMkRr9JhWQfY4F7jEpE2wvr2IHUFGc+pjS5SlW2OdlLw/ye +tL85/q7Sq/IJqWeD/Wu0MumeuNWTcj256NCL68M+erIlq+64patKR/d3qQIDAQAB +o1AwTjAJBgNVHRMEAjAAMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAL +BgNVHQ8EBAMCBaAwFQYDVR0RBA4wDIcEfwAAAYcECgABCjANBgkqhkiG9w0BAQ0F +AAOCAgEAJI1h0Ahk9RwwXxlEGksPHqOFrWsKHEw9iW/LhvCmWXJ9kTNoapDwECdr +VP7JlLmK7MUlYQufyX9BFKTdDXL57THls9in8IIAWCAqllMu9nN+d64dGTtb/LOQ +OfBtgmBmo8SYusiSfuFKpXmZLGB5zIbs5efV+7h4DN16LaQvJf+0/0ho8WDJD5OU +36UeHhoMY2euWhUjd5RctHW3BjBiRedei8F3pCKL/N5Jleao3yolBT17JDmvX5QS +oSlQIwON9V7RIOc3vIMC3jBRVFhIQe/UbpBraIds2Ao6X2K1XzVCtl45DpwZDj/h +aPbckad0C19/gXhVhaWaKHtkGehUunf8YuMf4igJTSgImG75s7iQMkxppaVySYgD +vUU2Fahm3LWYfW6AfFzy3f9ikbeQx53OmtrhYsgHKnvVMbXED0RZGlgLejVtG+fW +FVsdSN2O9xOR/00yGgZ8ATjd652oYzxgMyl95LQI6BIb2u48KOvltcZRQY2CfzLJ +UPVBohWxZOHsAeuyLktgVzJ5NhLnymwojZJfdBDzso12m+z2njSBKmJJoMoTTZ+S +9nwS3Hb7Ecyu5vJqJyt9BMmRzi/Yq2vwTuO0Y+E4yPrjQUOaLIwxE7jM7OIkbpBn +2qRZaK32bnz92PXxjONdgKoh2RTKm80lu9IZHPUE7y63Qlk1Ld8= +-----END CERTIFICATE----- diff --git a/test/certs/etcd-client.csr b/test/certs/etcd-client.csr new file mode 100644 index 0000000..738c87c --- /dev/null +++ b/test/certs/etcd-client.csr @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIEuDCCAqACAQAwNTELMAkGA1UEBhMCVVMxFDASBgNVBAMMC2V0Y2QtY2xpZW50 +MRAwDgYDVQQKDAdldGNkLWNhMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC +AgEAnLdhMvDQT3a96WGOy/BkpEjhfR24U7NHVq4haxMckN1in8ZE1xqrXx9FKGNX +/2DIx55z5KgI6gW4PpLsxpAHVtJ/M3u66uPo+xHbf8EgzSNbv0RQoj5ZXhRSbCfR +vWw81BRKesrUfjiYjYUb4nuJwBxhwuRwI53xcgLOWTtlZm0EVDIPbepaDWh7QLui +lQ2nYML8M18mZj7JP3ayWgpNj6LjxXLj714G/DgthxLeK2KRG7RFvsOPzCE28W+j +tk3AGAkXD23TvakljWSbqMrAvL6WpFHfoEjBB4e2xVo7jG+csKyX0kEFfB2/fPf6 +Jqus1lRgTiFBKqpHhMuzPeIBwz96MibmysvD/C7lY0+fR0SnpbFVI86ddaEIAvfb +8mNeOsI18K2pp4jilXotwA4ZvoN5v0g7UVvnZ2tF22mrLiP0hh8r5sCQ3OYWj4K8 +Q+/DdiNo2R6ZN29AN9CySdPaH0k6w/gZ4WanmiJWxs73hd839/QvdFVm86n1Ebtn +lzsxTPvnZxyQa/IvVrttzyjguzWHrdxpAuN5JDjydc9SgcFCyPROCLR89fz0Glrk +b3RlgCoSrVBzYggjbDChicaSvxtsIo8489KexW16gpFjPexY7tXPl+sDbRT8Qjqk +33r0xsE3qjeY4EbCmeRfe/Mf7FImitXxuJfSCxewqtK1iqcCAwEAAaA+MDwGCSqG +SIb3DQEJDjEvMC0wCQYDVR0TBAIwADATBgNVHSUEDDAKBggrBgEFBQcDAjALBgNV +HQ8EBAMCBaAwDQYJKoZIhvcNAQELBQADggIBAEAmerskdQikcyoId53QIjEBmYaB +93LtbvWBtxGb3NtP5wot5sF8mXpC5XPJe6Yn/haalH8yGUuDfiPZUC//VqzRnz+0 +yPsXDcjg34t27VZAkc2yDBGq1VQ7PsvCsM1BN/q/ywecKIvFLVTjuK2Rl3A7Nhyw +JvS1Nv47GZKg6TiGIMFsdjYFR4XiIuoOSW75NwbjxwBqKBAJRNh/iVZJ0IG7kKsg +z5kb+m22ekVSXkvxSKsqTF6sDJM2CAXsmv6rQmh1bLKP9T4Hf9aiM+lgqGFSKFSS +lUhNp12zBDlxwEmYbI7DUp1WnE+bsdAcX/mzL2BUVLGU+OdsM8pLEOpRJxKJYo0Q +Hif5cpmNB3t9mdXVDMrTrA/XNUAIkWclqI6zW857OeT2LNidtRlvFTVf4JDzosmB +EZPBh3j+Cu03zZOuUvQVOzZYZIZx4oYTtMSTT75pM622RqOl2Kk+4cRCjd0Grb0B +yFvZTzHdIVA0Ay3YEEOsbOxnz2nV4Y4v6q6xUhzDL5XudWW5iCySc9ql6ZQRfxMQ +M+9sAFn5d7Ckr6kSM/Vzf/txedIvOSwaRrf8p7ZdosKys6+hHPnsihvxiEhWgrET +/zZxNw5pZYpbczN0C92e3t2iGbWADSW16zVYJ1VkENGHqiHe5qjlwB7iPo3Db1Mp +pUWbCW6IjndkAPmh +-----END CERTIFICATE REQUEST----- diff --git a/test/certs/etcd0.localhost.csr b/test/certs/etcd0.localhost.csr new file mode 100644 index 0000000..f98463f --- /dev/null +++ b/test/certs/etcd0.localhost.csr @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIEvDCCAqQCAQAwOTELMAkGA1UEBhMCVVMxGDAWBgNVBAMMD2V0Y2QwLmxvY2Fs +aG9zdDEQMA4GA1UECgwHZXRjZC1jYTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC +AgoCggIBANrDVkM/fVBI3ZsWT8EF262dro9Oqgv02AShOSfc3HUF54i2suC0Osz+ +GbNbO+p0YyxwWMj0QpjsXnjINiEVOAothGGTVlor6cphpf7JopWPaa8UKyT6GgUk +I1ehmnwJ9G15HRe0AV4HyizEmrulwTcSko1RBhpcwnNW1H6+qVmAOhELtBP5ZNPT +hpxYoOIOZ3bZKDdjm4lzYEPWULPHOS9OhYLfobCHnVpwuBUM52NVcyZNXrs0JCOe +qzt+jHGqmGpBLB3OD3z9/bHIwCgN0Y5jkO3HDhtfm8mAbHLunGf06NgndYh4Gq6Q +hUn456rOwlfLH/um6ovC5+IT9Me5CtCvPpSHwDe1j9W5ahonkOemfgaYXi/snasd +5iK9vqGC2yJf9ezupL/oiW7q2m+piOhoduNBGYgy+hZlQairXGxshCZxEDNoip87 +/rV8Ni0YoC2skhitTWysFdB3eq0Jhj+0rAwcCYOaWTclLQvRRAQ72T48+0jeHujg +d6tt9ktBvXJlY4DTvqXTxQAlX30p/Ibc/I4Wksx618viQjditRKRaXFWhHrkykN3 +sAVetaQ1nYOl8yRGv0mFZB9jgXuMSkTbC+vYgdQUZz6mNLlKVbY52UvD/J60vzn+ +rtKr8gmpZ4P9a7Qy6Z641ZNyPbno0Ivrwz56siWr7rilq0pH93epAgMBAAGgPjA8 +BgkqhkiG9w0BCQ4xLzAtMAkGA1UdEwQCMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwIw +CwYDVR0PBAQDAgWgMA0GCSqGSIb3DQEBCwUAA4ICAQBtqTYafzl3HF98FpAaBBOb +tLqdV4Q98YgX0wLiqSEsSomDT1TbhdDODXiwPNBefIEhXH1TKCd/a54MzQ/JyjS8 +sQiVthG/Rf2UTc+0xoGs0sosPE9I2K+m7WwKSm6E6+BHwQ3cauPRupG3CkJqPgSX +TQ6dg4dqqjH6UdNVwYwIOC4XCS6CcJChIodiYFlqWvPOMpXlw0Tce4AtjRUSMoYj +SUselwIhlGsE6Hzq6M52qzjOb2GbR2RjXgdc0dn+8izwPAhgEa68emzjx/KMfjaH +9RRpOZ+EgIPE1eYhZViwyM5VM3tB2qkAnFYqPSzlUte/bh4rT7wAEGUnItpV/bqq +HnYucMyKEz5rysROBlDb93szAZxzJCuBTrVd5O2XpclFngnEvAWOt316IzvW/Yvu +3IK3RACLQqqoPDktyYxcFlY6dI0jSG8XB/ZZGeyi/FVOZ/sk0guuwQeqjb/Rx3zp +V33GFZoR/7yiwkA3c49DhbioBYuUQXuiZQuL1MnE9QeYX9GjIRwX9shyB93jr2VZ +58aJSQu5/tdwyFNaUeZ+EO2ZAY455nlKTJxsHMwVHLXtMsJXIm8/ikdzxLuMg8mt +DO9LtgiPbHce+7bkiGcVw2fjH+ZmfJAlqrKR1+/d6kAPsPrB09NhenEQ+oFpGcaQ +TneEPnjcCS3PE0CBCHs8yg== +-----END CERTIFICATE REQUEST----- diff --git a/test/certs/newcerts/01.pem b/test/certs/newcerts/01.pem new file mode 100644 index 0000000..ad3aaa5 --- /dev/null +++ b/test/certs/newcerts/01.pem @@ -0,0 +1,119 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 1 (0x1) + Signature Algorithm: sha512WithRSAEncryption + Issuer: C=US, CN=ca.etcd.localhost, O=etcd-ca + Validity + Not Before: May 31 15:20:32 2017 GMT + Not After : May 29 15:20:32 2027 GMT + Subject: O=etcd-ca, CN=etcd0.localhost + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (4096 bit) + Modulus: + 00:da:c3:56:43:3f:7d:50:48:dd:9b:16:4f:c1:05: + db:ad:9d:ae:8f:4e:aa:0b:f4:d8:04:a1:39:27:dc: + dc:75:05:e7:88:b6:b2:e0:b4:3a:cc:fe:19:b3:5b: + 3b:ea:74:63:2c:70:58:c8:f4:42:98:ec:5e:78:c8: + 36:21:15:38:0a:2d:84:61:93:56:5a:2b:e9:ca:61: + a5:fe:c9:a2:95:8f:69:af:14:2b:24:fa:1a:05:24: + 23:57:a1:9a:7c:09:f4:6d:79:1d:17:b4:01:5e:07: + ca:2c:c4:9a:bb:a5:c1:37:12:92:8d:51:06:1a:5c: + c2:73:56:d4:7e:be:a9:59:80:3a:11:0b:b4:13:f9: + 64:d3:d3:86:9c:58:a0:e2:0e:67:76:d9:28:37:63: + 9b:89:73:60:43:d6:50:b3:c7:39:2f:4e:85:82:df: + a1:b0:87:9d:5a:70:b8:15:0c:e7:63:55:73:26:4d: + 5e:bb:34:24:23:9e:ab:3b:7e:8c:71:aa:98:6a:41: + 2c:1d:ce:0f:7c:fd:fd:b1:c8:c0:28:0d:d1:8e:63: + 90:ed:c7:0e:1b:5f:9b:c9:80:6c:72:ee:9c:67:f4: + e8:d8:27:75:88:78:1a:ae:90:85:49:f8:e7:aa:ce: + c2:57:cb:1f:fb:a6:ea:8b:c2:e7:e2:13:f4:c7:b9: + 0a:d0:af:3e:94:87:c0:37:b5:8f:d5:b9:6a:1a:27: + 90:e7:a6:7e:06:98:5e:2f:ec:9d:ab:1d:e6:22:bd: + be:a1:82:db:22:5f:f5:ec:ee:a4:bf:e8:89:6e:ea: + da:6f:a9:88:e8:68:76:e3:41:19:88:32:fa:16:65: + 41:a8:ab:5c:6c:6c:84:26:71:10:33:68:8a:9f:3b: + fe:b5:7c:36:2d:18:a0:2d:ac:92:18:ad:4d:6c:ac: + 15:d0:77:7a:ad:09:86:3f:b4:ac:0c:1c:09:83:9a: + 59:37:25:2d:0b:d1:44:04:3b:d9:3e:3c:fb:48:de: + 1e:e8:e0:77:ab:6d:f6:4b:41:bd:72:65:63:80:d3: + be:a5:d3:c5:00:25:5f:7d:29:fc:86:dc:fc:8e:16: + 92:cc:7a:d7:cb:e2:42:37:62:b5:12:91:69:71:56: + 84:7a:e4:ca:43:77:b0:05:5e:b5:a4:35:9d:83:a5: + f3:24:46:bf:49:85:64:1f:63:81:7b:8c:4a:44:db: + 0b:eb:d8:81:d4:14:67:3e:a6:34:b9:4a:55:b6:39: + d9:4b:c3:fc:9e:b4:bf:39:fe:ae:d2:ab:f2:09:a9: + 67:83:fd:6b:b4:32:e9:9e:b8:d5:93:72:3d:b9:e8: + d0:8b:eb:c3:3e:7a:b2:25:ab:ee:b8:a5:ab:4a:47: + f7:77:a9 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + X509v3 Extended Key Usage: + TLS Web Client Authentication, TLS Web Server Authentication + X509v3 Key Usage: + Digital Signature, Key Encipherment + X509v3 Subject Alternative Name: + IP Address:127.0.0.1, IP Address:10.0.1.10 + Signature Algorithm: sha512WithRSAEncryption + 24:8d:61:d0:08:64:f5:1c:30:5f:19:44:1a:4b:0f:1e:a3:85: + ad:6b:0a:1c:4c:3d:89:6f:cb:86:f0:a6:59:72:7d:91:33:68: + 6a:90:f0:10:27:6b:54:fe:c9:94:b9:8a:ec:c5:25:61:0b:9f: + c9:7f:41:14:a4:dd:0d:72:f9:ed:31:e5:b3:d8:a7:f0:82:00: + 58:20:2a:96:53:2e:f6:73:7e:77:ae:1d:19:3b:5b:fc:b3:90: + 39:f0:6d:82:60:66:a3:c4:98:ba:c8:92:7e:e1:4a:a5:79:99: + 2c:60:79:cc:86:ec:e5:e7:d5:fb:b8:78:0c:dd:7a:2d:a4:2f: + 25:ff:b4:ff:48:68:f1:60:c9:0f:93:94:df:a5:1e:1e:1a:0c: + 63:67:ae:5a:15:23:77:94:5c:b4:75:b7:06:30:62:45:e7:5e: + 8b:c1:77:a4:22:8b:fc:de:49:95:e6:a8:df:2a:25:05:3d:7b: + 24:39:af:5f:94:12:a1:29:50:23:03:8d:f5:5e:d1:20:e7:37: + bc:83:02:de:30:51:54:58:48:41:ef:d4:6e:90:6b:68:87:6c: + d8:0a:3a:5f:62:b5:5f:35:42:b6:5e:39:0e:9c:19:0e:3f:e1: + 68:f6:dc:91:a7:74:0b:5f:7f:81:78:55:85:a5:9a:28:7b:64: + 19:e8:54:ba:77:fc:62:e3:1f:e2:28:09:4d:28:08:98:6e:f9: + b3:b8:90:32:4c:69:a5:a5:72:49:88:03:bd:45:36:15:a8:66: + dc:b5:98:7d:6e:80:7c:5c:f2:dd:ff:62:91:b7:90:c7:9d:ce: + 9a:da:e1:62:c8:07:2a:7b:d5:31:b5:c4:0f:44:59:1a:58:0b: + 7a:35:6d:1b:e7:d6:15:5b:1d:48:dd:8e:f7:13:91:ff:4d:32: + 1a:06:7c:01:38:dd:eb:9d:a8:63:3c:60:33:29:7d:e4:b4:08: + e8:12:1b:da:ee:3c:28:eb:e5:b5:c6:51:41:8d:82:7f:32:c9: + 50:f5:41:a2:15:b1:64:e1:ec:01:eb:b2:2e:4b:60:57:32:79: + 36:12:e7:ca:6c:28:8d:92:5f:74:10:f3:b2:8d:76:9b:ec:f6: + 9e:34:81:2a:62:49:a0:ca:13:4d:9f:92:f6:7c:12:dc:76:fb: + 11:cc:ae:e6:f2:6a:27:2b:7d:04:c9:91:ce:2f:d8:ab:6b:f0: + 4e:e3:b4:63:e1:38:c8:fa:e3:41:43:9a:2c:8c:31:13:b8:cc: + ec:e2:24:6e:90:67:da:a4:59:68:ad:f6:6e:7c:fd:d8:f5:f1: + 8c:e3:5d:80:aa:21:d9:14:ca:9b:cd:25:bb:d2:19:1c:f5:04: + ef:2e:b7:42:59:35:2d:df +-----BEGIN CERTIFICATE----- +MIIFMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQ0FADA7MQswCQYDVQQGEwJVUzEa +MBgGA1UEAwwRY2EuZXRjZC5sb2NhbGhvc3QxEDAOBgNVBAoMB2V0Y2QtY2EwHhcN +MTcwNTMxMTUyMDMyWhcNMjcwNTI5MTUyMDMyWjAsMRAwDgYDVQQKDAdldGNkLWNh +MRgwFgYDVQQDDA9ldGNkMC5sb2NhbGhvc3QwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQDaw1ZDP31QSN2bFk/BBdutna6PTqoL9NgEoTkn3Nx1BeeItrLg +tDrM/hmzWzvqdGMscFjI9EKY7F54yDYhFTgKLYRhk1ZaK+nKYaX+yaKVj2mvFCsk ++hoFJCNXoZp8CfRteR0XtAFeB8osxJq7pcE3EpKNUQYaXMJzVtR+vqlZgDoRC7QT ++WTT04acWKDiDmd22Sg3Y5uJc2BD1lCzxzkvToWC36Gwh51acLgVDOdjVXMmTV67 +NCQjnqs7foxxqphqQSwdzg98/f2xyMAoDdGOY5Dtxw4bX5vJgGxy7pxn9OjYJ3WI +eBqukIVJ+OeqzsJXyx/7puqLwufiE/THuQrQrz6Uh8A3tY/VuWoaJ5Dnpn4GmF4v +7J2rHeYivb6hgtsiX/Xs7qS/6Ilu6tpvqYjoaHbjQRmIMvoWZUGoq1xsbIQmcRAz +aIqfO/61fDYtGKAtrJIYrU1srBXQd3qtCYY/tKwMHAmDmlk3JS0L0UQEO9k+PPtI +3h7o4HerbfZLQb1yZWOA076l08UAJV99KfyG3PyOFpLMetfL4kI3YrUSkWlxVoR6 +5MpDd7AFXrWkNZ2DpfMkRr9JhWQfY4F7jEpE2wvr2IHUFGc+pjS5SlW2OdlLw/ye +tL85/q7Sq/IJqWeD/Wu0MumeuNWTcj256NCL68M+erIlq+64patKR/d3qQIDAQAB +o1AwTjAJBgNVHRMEAjAAMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAL +BgNVHQ8EBAMCBaAwFQYDVR0RBA4wDIcEfwAAAYcECgABCjANBgkqhkiG9w0BAQ0F +AAOCAgEAJI1h0Ahk9RwwXxlEGksPHqOFrWsKHEw9iW/LhvCmWXJ9kTNoapDwECdr +VP7JlLmK7MUlYQufyX9BFKTdDXL57THls9in8IIAWCAqllMu9nN+d64dGTtb/LOQ +OfBtgmBmo8SYusiSfuFKpXmZLGB5zIbs5efV+7h4DN16LaQvJf+0/0ho8WDJD5OU +36UeHhoMY2euWhUjd5RctHW3BjBiRedei8F3pCKL/N5Jleao3yolBT17JDmvX5QS +oSlQIwON9V7RIOc3vIMC3jBRVFhIQe/UbpBraIds2Ao6X2K1XzVCtl45DpwZDj/h +aPbckad0C19/gXhVhaWaKHtkGehUunf8YuMf4igJTSgImG75s7iQMkxppaVySYgD +vUU2Fahm3LWYfW6AfFzy3f9ikbeQx53OmtrhYsgHKnvVMbXED0RZGlgLejVtG+fW +FVsdSN2O9xOR/00yGgZ8ATjd652oYzxgMyl95LQI6BIb2u48KOvltcZRQY2CfzLJ +UPVBohWxZOHsAeuyLktgVzJ5NhLnymwojZJfdBDzso12m+z2njSBKmJJoMoTTZ+S +9nwS3Hb7Ecyu5vJqJyt9BMmRzi/Yq2vwTuO0Y+E4yPrjQUOaLIwxE7jM7OIkbpBn +2qRZaK32bnz92PXxjONdgKoh2RTKm80lu9IZHPUE7y63Qlk1Ld8= +-----END CERTIFICATE----- diff --git a/test/certs/newcerts/02.pem b/test/certs/newcerts/02.pem new file mode 100644 index 0000000..83b3154 --- /dev/null +++ b/test/certs/newcerts/02.pem @@ -0,0 +1,117 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 2 (0x2) + Signature Algorithm: sha512WithRSAEncryption + Issuer: C=US, CN=ca.etcd.localhost, O=etcd-ca + Validity + Not Before: May 31 15:21:47 2017 GMT + Not After : May 29 15:21:47 2027 GMT + Subject: O=etcd-ca, CN=etcd-client + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (4096 bit) + Modulus: + 00:9c:b7:61:32:f0:d0:4f:76:bd:e9:61:8e:cb:f0: + 64:a4:48:e1:7d:1d:b8:53:b3:47:56:ae:21:6b:13: + 1c:90:dd:62:9f:c6:44:d7:1a:ab:5f:1f:45:28:63: + 57:ff:60:c8:c7:9e:73:e4:a8:08:ea:05:b8:3e:92: + ec:c6:90:07:56:d2:7f:33:7b:ba:ea:e3:e8:fb:11: + db:7f:c1:20:cd:23:5b:bf:44:50:a2:3e:59:5e:14: + 52:6c:27:d1:bd:6c:3c:d4:14:4a:7a:ca:d4:7e:38: + 98:8d:85:1b:e2:7b:89:c0:1c:61:c2:e4:70:23:9d: + f1:72:02:ce:59:3b:65:66:6d:04:54:32:0f:6d:ea: + 5a:0d:68:7b:40:bb:a2:95:0d:a7:60:c2:fc:33:5f: + 26:66:3e:c9:3f:76:b2:5a:0a:4d:8f:a2:e3:c5:72: + e3:ef:5e:06:fc:38:2d:87:12:de:2b:62:91:1b:b4: + 45:be:c3:8f:cc:21:36:f1:6f:a3:b6:4d:c0:18:09: + 17:0f:6d:d3:bd:a9:25:8d:64:9b:a8:ca:c0:bc:be: + 96:a4:51:df:a0:48:c1:07:87:b6:c5:5a:3b:8c:6f: + 9c:b0:ac:97:d2:41:05:7c:1d:bf:7c:f7:fa:26:ab: + ac:d6:54:60:4e:21:41:2a:aa:47:84:cb:b3:3d:e2: + 01:c3:3f:7a:32:26:e6:ca:cb:c3:fc:2e:e5:63:4f: + 9f:47:44:a7:a5:b1:55:23:ce:9d:75:a1:08:02:f7: + db:f2:63:5e:3a:c2:35:f0:ad:a9:a7:88:e2:95:7a: + 2d:c0:0e:19:be:83:79:bf:48:3b:51:5b:e7:67:6b: + 45:db:69:ab:2e:23:f4:86:1f:2b:e6:c0:90:dc:e6: + 16:8f:82:bc:43:ef:c3:76:23:68:d9:1e:99:37:6f: + 40:37:d0:b2:49:d3:da:1f:49:3a:c3:f8:19:e1:66: + a7:9a:22:56:c6:ce:f7:85:df:37:f7:f4:2f:74:55: + 66:f3:a9:f5:11:bb:67:97:3b:31:4c:fb:e7:67:1c: + 90:6b:f2:2f:56:bb:6d:cf:28:e0:bb:35:87:ad:dc: + 69:02:e3:79:24:38:f2:75:cf:52:81:c1:42:c8:f4: + 4e:08:b4:7c:f5:fc:f4:1a:5a:e4:6f:74:65:80:2a: + 12:ad:50:73:62:08:23:6c:30:a1:89:c6:92:bf:1b: + 6c:22:8f:38:f3:d2:9e:c5:6d:7a:82:91:63:3d:ec: + 58:ee:d5:cf:97:eb:03:6d:14:fc:42:3a:a4:df:7a: + f4:c6:c1:37:aa:37:98:e0:46:c2:99:e4:5f:7b:f3: + 1f:ec:52:26:8a:d5:f1:b8:97:d2:0b:17:b0:aa:d2: + b5:8a:a7 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + X509v3 Extended Key Usage: + TLS Web Client Authentication + X509v3 Key Usage: + Digital Signature, Key Encipherment + Signature Algorithm: sha512WithRSAEncryption + b6:a5:6c:5b:8c:a4:dc:5c:a5:43:f0:c0:47:49:15:c2:81:d9: + 58:b7:53:41:f1:cc:e8:14:f2:2e:76:59:2d:d7:08:3e:e1:72: + 52:0a:30:79:fc:76:a3:ee:a6:d1:07:7d:bc:4b:97:62:5b:e0: + 69:62:7e:4d:0e:f7:2b:be:11:c2:56:3d:31:89:12:c1:0d:34: + 82:87:e8:d6:aa:92:d4:5c:d6:7c:b1:46:4d:87:de:7d:20:12: + 7f:35:76:f6:27:ef:0c:84:c3:e9:95:dd:c6:07:7e:d5:4d:5d: + c2:e7:63:5b:cd:59:59:ec:87:32:64:10:19:9b:8d:e1:b1:8a: + 28:a3:0a:ba:30:8c:9d:15:63:ea:a2:42:90:34:7b:a7:aa:9b: + 95:b2:5e:a6:0d:50:29:67:8f:2e:c6:f9:8d:0b:3a:e3:f5:11: + 27:d3:92:97:5a:62:f8:b2:ef:c4:b2:2a:c7:32:c6:13:bf:c8: + 89:76:19:43:bb:d0:52:0c:14:af:f2:67:9d:f6:32:2d:2f:18: + e0:ac:5f:d0:e6:89:43:0b:06:8f:f0:33:78:86:cb:e6:07:64: + 62:7d:89:ca:06:58:23:a8:9b:39:2a:38:9f:18:93:f0:8b:d2: + bb:5f:b8:f7:55:46:1a:01:bf:c1:ea:b5:9c:01:72:65:ca:fa: + ec:63:ef:58:59:d0:7c:08:cf:25:3d:45:fb:4e:8d:51:7d:83: + 74:31:2f:0a:a1:38:02:c6:a3:77:6f:c9:10:87:72:14:e9:cb: + 89:82:e1:ce:3f:86:4e:66:a1:ca:40:bd:3a:27:81:a4:75:6c: + 25:8e:f0:16:b8:9c:45:3a:26:b4:06:53:24:e4:f9:d5:5f:1d: + 8e:55:23:cb:44:1f:90:dc:5e:74:b1:c3:4e:6d:8a:60:de:9a: + 45:16:8c:05:66:c4:84:9e:76:d9:d8:56:25:12:ff:9b:fc:97: + 4d:05:32:8a:9a:5e:ff:a4:e2:b0:64:1a:31:4b:b1:37:4a:fe: + 6b:a3:c5:49:ff:75:bf:1b:d0:81:16:70:a5:5a:af:75:ed:ed: + ed:95:15:57:68:5d:36:57:06:b5:81:15:6a:9f:03:78:5c:8e: + 9f:51:89:27:de:a6:ad:f3:5b:11:0a:04:be:7d:ca:a1:c5:f2: + 1a:c0:63:3c:67:f8:b7:f6:05:98:9e:b7:64:69:e8:15:69:fa: + 05:5f:20:dd:be:a2:bb:c0:8b:c4:f8:a9:96:04:6b:86:a5:18: + 43:eb:6d:1f:b9:d8:76:46:e2:df:a5:0e:bc:d8:43:62:9f:6f: + bc:77:2b:30:50:a1:de:4d:cd:58:a9:4b:49:9f:ee:88:ff:be: + 73:ff:e1:c0:f5:04:af:48 +-----BEGIN CERTIFICATE----- +MIIFDTCCAvWgAwIBAgIBAjANBgkqhkiG9w0BAQ0FADA7MQswCQYDVQQGEwJVUzEa +MBgGA1UEAwwRY2EuZXRjZC5sb2NhbGhvc3QxEDAOBgNVBAoMB2V0Y2QtY2EwHhcN +MTcwNTMxMTUyMTQ3WhcNMjcwNTI5MTUyMTQ3WjAoMRAwDgYDVQQKDAdldGNkLWNh +MRQwEgYDVQQDDAtldGNkLWNsaWVudDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC +AgoCggIBAJy3YTLw0E92velhjsvwZKRI4X0duFOzR1auIWsTHJDdYp/GRNcaq18f +RShjV/9gyMeec+SoCOoFuD6S7MaQB1bSfzN7uurj6PsR23/BIM0jW79EUKI+WV4U +Umwn0b1sPNQUSnrK1H44mI2FG+J7icAcYcLkcCOd8XICzlk7ZWZtBFQyD23qWg1o +e0C7opUNp2DC/DNfJmY+yT92sloKTY+i48Vy4+9eBvw4LYcS3itikRu0Rb7Dj8wh +NvFvo7ZNwBgJFw9t072pJY1km6jKwLy+lqRR36BIwQeHtsVaO4xvnLCsl9JBBXwd +v3z3+iarrNZUYE4hQSqqR4TLsz3iAcM/ejIm5srLw/wu5WNPn0dEp6WxVSPOnXWh +CAL32/JjXjrCNfCtqaeI4pV6LcAOGb6Deb9IO1Fb52drRdtpqy4j9IYfK+bAkNzm +Fo+CvEPvw3YjaNkemTdvQDfQsknT2h9JOsP4GeFmp5oiVsbO94XfN/f0L3RVZvOp +9RG7Z5c7MUz752cckGvyL1a7bc8o4Ls1h63caQLjeSQ48nXPUoHBQsj0Tgi0fPX8 +9Bpa5G90ZYAqEq1Qc2III2wwoYnGkr8bbCKPOPPSnsVteoKRYz3sWO7Vz5frA20U +/EI6pN969MbBN6o3mOBGwpnkX3vzH+xSJorV8biX0gsXsKrStYqnAgMBAAGjLzAt +MAkGA1UdEwQCMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwCwYDVR0PBAQDAgWgMA0G +CSqGSIb3DQEBDQUAA4ICAQC2pWxbjKTcXKVD8MBHSRXCgdlYt1NB8czoFPIudlkt +1wg+4XJSCjB5/Haj7qbRB328S5diW+BpYn5NDvcrvhHCVj0xiRLBDTSCh+jWqpLU +XNZ8sUZNh959IBJ/NXb2J+8MhMPpld3GB37VTV3C52NbzVlZ7IcyZBAZm43hsYoo +owq6MIydFWPqokKQNHunqpuVsl6mDVApZ48uxvmNCzrj9REn05KXWmL4su/EsirH +MsYTv8iJdhlDu9BSDBSv8med9jItLxjgrF/Q5olDCwaP8DN4hsvmB2RifYnKBlgj +qJs5KjifGJPwi9K7X7j3VUYaAb/B6rWcAXJlyvrsY+9YWdB8CM8lPUX7To1RfYN0 +MS8KoTgCxqN3b8kQh3IU6cuJguHOP4ZOZqHKQL06J4GkdWwljvAWuJxFOia0BlMk +5PnVXx2OVSPLRB+Q3F50scNObYpg3ppFFowFZsSEnnbZ2FYlEv+b/JdNBTKKml7/ +pOKwZBoxS7E3Sv5ro8VJ/3W/G9CBFnClWq917e3tlRVXaF02Vwa1gRVqnwN4XI6f +UYkn3qat81sRCgS+fcqhxfIawGM8Z/i39gWYnrdkaegVafoFXyDdvqK7wIvE+KmW +BGuGpRhD620fudh2RuLfpQ682ENin2+8dyswUKHeTc1YqUtJn+6I/75z/+HA9QSv +SA== +-----END CERTIFICATE----- diff --git a/test/certs/private/ca.key b/test/certs/private/ca.key new file mode 100644 index 0000000..8016d15 --- /dev/null +++ b/test/certs/private/ca.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +MJS8GsAhCrLBofSK4hSvUKj3x1qo +-----END PRIVATE KEY----- diff --git a/test/certs/private/etcd-client.key b/test/certs/private/etcd-client.key new file mode 100644 index 0000000..3a9a4d5 --- /dev/null +++ b/test/certs/private/etcd-client.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +lGlsn9EcTo64YBVsKed9ozJlDcJ7 +-----END PRIVATE KEY----- diff --git a/test/certs/private/etcd0.localhost.key b/test/certs/private/etcd0.localhost.key new file mode 100644 index 0000000..7456051 --- /dev/null +++ b/test/certs/private/etcd0.localhost.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +HsoCRI7O1n+/trrmcq1D7JW0FVcbNA== +-----END PRIVATE KEY----- diff --git a/test/certs/readme.md b/test/certs/readme.md new file mode 100644 index 0000000..996ef5c --- /dev/null +++ b/test/certs/readme.md @@ -0,0 +1 @@ +These certificates are self-signed certs created using [these instructions](https://github.com/kelseyhightower/etcd-production-setup). diff --git a/test/kv.test.ts b/test/client.test.ts similarity index 59% rename from test/kv.test.ts rename to test/client.test.ts index 22361d6..f1aad7f 100644 --- a/test/kv.test.ts +++ b/test/client.test.ts @@ -5,31 +5,47 @@ import { Etcd3, EtcdLeaseInvalidError, EtcdLockFailedError, + EtcdRoleExistsError, + EtcdRoleNotFoundError, + EtcdRoleNotGrantedError, + EtcdUserExistsError, + EtcdUserNotFoundError, GRPCConnectFailedError, Lease, + Role, } from '../src'; -import { getHosts } from './util'; +import { getOptions } from './util'; -describe('connection pool', () => { +function expectReject(promise: Promise, err: { new (message: string): Error }) { + return promise + .then(() => { throw new Error('expected to reject'); }) + .catch(actualErr => expect(actualErr).to.be.an.instanceof(err)); +} + +function wipeAll(things: Promise<{ delete(): any }[]>) { + return things.then(items => Promise.all(items.map(item => item.delete()))); +} + +describe('client', () => { let client: Etcd3; let badClient: Etcd3; - beforeEach(() => { - client = new Etcd3({ hosts: getHosts() }); - badClient = new Etcd3({ hosts: '127.0.0.1:1' }); - return Promise.all([ - client.put('foo1').value('bar1'), - client.put('foo2').value('bar2'), - client.put('foo3').value('{"value":"bar3"}'), - client.put('baz').value('bar5'), - ]); - }); + // beforeEach(() => { + // client = new Etcd3(getOptions()); + // badClient = new Etcd3(getOptions({ hosts: '127.0.0.1:1' })); + // return Promise.all([ + // client.put('foo1').value('bar1'), + // client.put('foo2').value('bar2'), + // client.put('foo3').value('{"value":"bar3"}'), + // client.put('baz').value('bar5'), + // ]); + // }); - afterEach(async () => { - await client.delete().all(); - client.close(); - badClient.close(); - }); + // afterEach(async () => { + // await client.delete().all(); + // client.close(); + // badClient.close(); + // }); it('allows mocking', async () => { const mock = client.mock({ @@ -95,14 +111,14 @@ describe('connection pool', () => { it('sorts', async () => { expect(await client.getAll() .prefix('foo') - .sort('key', 'asc') + .sort('key', 'ascend') .limit(2) .keys(), ).to.deep.equal(['1', '2']); expect(await client.getAll() .prefix('foo') - .sort('key', 'desc') + .sort('key', 'descend') .limit(2) .keys(), ).to.deep.equal(['3', '2']); @@ -311,4 +327,169 @@ describe('connection pool', () => { }); }); }); + + describe('roles', () => { + afterEach(() => wipeAll(client.getRoles())); + + const expectRoles = async (expected: string[]) => { + const list = await client.getRoles(); + expect(list.map(r => r.name)).to.deep.equal(expected); + }; + + it('create and deletes', async () => { + const fooRole = await client.role('foo').create(); + expectRoles(['foo']); + await fooRole.delete(); + expectRoles([]); + }); + + it('throws on existing roles', async () => { + await client.role('foo').create(); + await expectReject(client.role('foo').create(), EtcdRoleExistsError); + }); + + it('throws on deleting a non-existent role', async () => { + await expectReject(client.role('foo').delete(), EtcdRoleNotFoundError); + }); + + it('throws on granting permission to a non-existent role', async () => { + await expectReject( + client.role('foo').grant({ + permission: 'read', + range: client.range({ prefix: '111' }), + }), + EtcdRoleNotFoundError, + ); + }); + + it('round trips permission grants', async () => { + const fooRole = await client.role('foo').create(); + await fooRole.grant({ + permission: 'read', + range: client.range({ prefix: '111' }), + }); + + const perms = await fooRole.permissions(); + expect(perms).to.containSubset([ + { + permission: 'read', + range: client.range({ prefix: '111' }), + }, + ]); + + await fooRole.revoke(perms[0]); + expect(await fooRole.permissions()).to.have.length(0); + }); + }); + + describe('users', () => { + let fooRole: Role; + beforeEach(async () => { + fooRole = client.role('foo'); + await fooRole.create(); + }); + + afterEach(async () => { + await fooRole.delete(); + await wipeAll(client.getUsers()); + }); + + it('creates users', async () => { + expect(await client.getUsers()).to.have.lengthOf(0); + await client.user('connor').create('password'); + expect(await client.getUsers()).to.containSubset([{ name: 'connor' }]); + }); + + it('throws on existing users', async () => { + await client.user('connor').create('password'); + await expectReject(client.user('connor').create('password'), EtcdUserExistsError); + }); + + it('throws on regranting the same role multiple times', async () => { + const user = await client.user('connor').create('password'); + await expectReject(user.removeRole(fooRole), EtcdRoleNotGrantedError); + }); + + it('throws on granting a non-existent role', async () => { + const user = await client.user('connor').create('password'); + await expectReject(user.addRole('wut'), EtcdRoleNotFoundError); + }); + + it('throws on deleting a non-existent user', async () => { + await expectReject(client.user('connor').delete(), EtcdUserNotFoundError); + }); + + it('round trips roles', async () => { + const user = await client.user('connor').create('password'); + await user.addRole(fooRole); + expect(await user.roles()).to.containSubset([{ name: 'foo' }]); + await user.removeRole(fooRole); + expect(await user.roles()).to.have.lengthOf(0); + }); + }); + + describe('password auth', () => { + // beforeEach(async () => { + // await wipeAll(client.getUsers()); + // await wipeAll(client.getRoles()); + + // // We need to set up a root user and root role first, otherwise etcd + // // will yell at us. + // const rootUser = await client.user('root').create('password'); + // rootUser.addRole('root'); + + // await client.user('connor').create('password'); + + // const normalRole = await client.role('all').create(); + // await normalRole.grant({ + // permission: 'readwrite', + // range: client.range({ prefix: 'f' }), + // }); + // await normalRole.addUser('connor'); + // await client.auth.authEnable(); + // }); + + // afterEach(async () => { + // const rootClient = new Etcd3(getOptions({ + // auth: { + // username: 'root', + // password: 'password', + // }, + // })); + + // await rootClient.auth.authDisable(); + // rootClient.close(); + + // await wipeAll(client.getUsers()); + // await wipeAll(client.getRoles()); + // }); + + it('allows authentication using the correct credentials', async () => { + const authedClient = new Etcd3(getOptions({ + auth: { + username: 'root', + password: 'password', + }, + })); + + await authedClient.put('foo').value('bar'); + authedClient.close(); + }); + + it('throws when using incorrect credentials', async () => { + const authedClient = new Etcd3(getOptions({ + auth: { + username: 'connor', + password: 'password', + }, + })); + + await expectReject( + authedClient.put('foo').value('bar').exec(), + EtcdRoleNotFoundError, + ); + + authedClient.close(); + }); + }); }); diff --git a/test/connection-pool.test.ts b/test/connection-pool.test.ts index 608bf82..361f4d2 100644 --- a/test/connection-pool.test.ts +++ b/test/connection-pool.test.ts @@ -1,9 +1,16 @@ import { expect } from 'chai'; import { ConnectionPool } from '../src/connection-pool'; -import { GRPCConnectFailedError } from '../src/errors'; import { KVClient } from '../src/rpc'; -import { getHosts } from './util'; +import { GRPCConnectFailedError, IOptions } from '../src'; +import { getOptions, getHost } from './util'; + +function getOptionsWithBadHost(options: Partial = {}): IOptions { + return getOptions({ + hosts: ['127.0.0.1:1', getHost()], + ...options, + }); +} describe('connection pool', () => { const key = new Buffer('foo'); @@ -18,7 +25,7 @@ describe('connection pool', () => { }); it('calls simple methods', async () => { - pool = new ConnectionPool({ hosts: getHosts() }); + pool = new ConnectionPool(getOptions()); const kv = new KVClient(pool); await kv.put({ key, value }); const res = await kv.range({ key }); @@ -28,7 +35,7 @@ describe('connection pool', () => { }); it('rejects hitting invalid hosts', () => { - pool = new ConnectionPool({ hosts: ['127.0.0.1:1', getHosts()] }); + pool = new ConnectionPool(getOptionsWithBadHost()); const kv = new KVClient(pool); return kv.range({ key }) .then(() => { throw new Error('expected to reject'); }) @@ -36,10 +43,7 @@ describe('connection pool', () => { }); it('retries when requested', async () => { - pool = new ConnectionPool({ - hosts: ['127.0.0.1:1', getHosts()], - retry: true, - }); + pool = new ConnectionPool(getOptionsWithBadHost({ retry: true })); const kv = new KVClient(pool); expect((await kv.range({ key })).kvs).to.deep.equal([]); }); diff --git a/test/memoize.test.ts b/test/memoize.test.ts new file mode 100644 index 0000000..5c27b65 --- /dev/null +++ b/test/memoize.test.ts @@ -0,0 +1,69 @@ +import { expect } from 'chai'; +import { forget, Memoize } from '../src/memoize'; + +class Foo { + public incr = 0; + + @Memoize() + public get incrGetter(): number { + this.incr += 1; + return this.incr; + } + + @Memoize() + public basicMemoized(amount: number): number { + this.incr += amount; + return this.incr; + } + + @Memoize((value: number) => value % 2) + public byModulo(amount: number): number { + this.incr += amount; + return this.incr; + } + + public forgetBasic() { + forget(this, this.basicMemoized); + } +} + +describe('@Memoize', () => { + it('memoizes simple methods', () => { + const foo = new Foo(); + expect(foo.basicMemoized(1)).to.equal(1); + expect(foo.basicMemoized(1)).to.equal(1); + expect(foo.basicMemoized(2)).to.equal(3); + }); + it('forgets memoized values', () => { + const foo = new Foo(); + expect(foo.basicMemoized(1)).to.equal(1); + foo.forgetBasic(); + expect(foo.basicMemoized(1)).to.equal(2); + expect(foo.basicMemoized(1)).to.equal(2); + }); + + it('memoizes with custom hashers', () => { + const foo = new Foo(); + expect(foo.incr).to.equal(0); + expect(foo.byModulo(1)).to.equal(1); + expect(foo.byModulo(2)).to.equal(3); + expect(foo.byModulo(3)).to.equal(1); + expect(foo.byModulo(4)).to.equal(3); + }); + + it('memoizes getters', () => { + const foo = new Foo(); + expect(foo.incrGetter).to.equal(1); + expect(foo.incrGetter).to.equal(1); + }); + + it('throws when attaching to a non-memoizable type', () => { + expect(() => { + class Bar { + @Memoize() + public set foo(_value: number) { /* noop */ } + } + return Bar; + }).to.throw(/Can only attach/); + }); +}); diff --git a/test/range.test.ts b/test/range.test.ts new file mode 100644 index 0000000..39e6345 --- /dev/null +++ b/test/range.test.ts @@ -0,0 +1,55 @@ +import { expect } from 'chai'; +import { Range } from '../src/range'; + +describe('Range', () => { + describe('prefix', () => { + it('generates prefixes for an empty string', () => { + const range = Range.prefix(Buffer.from([])); + expect(range.start).to.deep.equal(Buffer.from([0])); + expect(range.end).to.deep.equal(Buffer.from([0])); + }); + + it('generates prefixes for a "normal" string', () => { + const range = Range.prefix(Buffer.from([1, 2])); + expect(range.start).to.deep.equal(Buffer.from([1, 2])); + expect(range.end).to.deep.equal(Buffer.from([1, 3])); + }); + + it('rolls on a high end-bit', () => { + const range = Range.prefix(Buffer.from([1, 255])); + expect(range.start).to.deep.equal(Buffer.from([1, 255])); + expect(range.end).to.deep.equal(Buffer.from([2])); + }); + + it('aborts on all high bits', () => { + const range = Range.prefix(Buffer.from([255, 255])); + expect(range.start).to.deep.equal(Buffer.from([255, 255])); + expect(range.end).to.deep.equal(Buffer.from([0])); + }); + }); + + describe('comparisons', () => { + const prefix: Buffer[] = []; + for (let i = 0; i < 10; i += 1) { + prefix.push(Buffer.from([i])); + } + + it('compares ranges', () => { + const r = new Range(prefix[2], prefix[5]); + expect(r.compare(new Range(prefix[2], prefix[5]))).to.equal(0); + expect(r.compare(new Range(prefix[3], prefix[6]))).to.equal(0); + expect(r.compare(new Range(prefix[0], prefix[4]))).to.equal(0); + expect(r.compare(new Range(prefix[0], prefix[9]))).to.equal(0); + expect(r.compare(new Range(prefix[3], prefix[4]))).to.equal(0); + expect(r.compare(new Range(prefix[5], prefix[7]))).to.equal(-1); + expect(r.compare(new Range(prefix[0], prefix[1]))).to.equal(1); + }); + + it('checks if a key is included', () => { + const r = new Range(prefix[2], prefix[5]); + expect(r.includes(prefix[1])).to.be.false; + expect(r.includes(prefix[2])).to.be.true; + expect(r.includes(prefix[5])).to.be.false; + }); + }); +}); diff --git a/test/util.ts b/test/util.ts index e87b076..f4adb47 100644 --- a/test/util.ts +++ b/test/util.ts @@ -1,6 +1,22 @@ +import { IOptions } from '../src'; +import * as fs from 'fs'; + +const rootCertificate = fs.readFileSync(`${__dirname}/certs/certs/ca.crt`); + /** - * Returns etcd hosts to test against. + * Returns the host to test against. */ -export function getHosts(): string { +export function getHost(): string { return process.env.ETCD_ADDR || '127.0.0.1:2379'; } + +/** + * Returns etcd options to use for connections. + */ +export function getOptions(defaults: Partial = {}): IOptions { + return { + hosts: getHost(), + credentials: { rootCertificate }, + ...defaults, + }; +} diff --git a/tsconfig.json b/tsconfig.json index 89a6efb..d1ab9cd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,7 @@ "noUnusedLocals": true, "noUnusedParameters": true, "strictNullChecks": true, + "experimentalDecorators": true, "module": "commonjs", "target": "es6", "outDir": "lib", diff --git a/tslint.json b/tslint.json index 886b7fd..a9058f3 100644 --- a/tslint.json +++ b/tslint.json @@ -4,30 +4,73 @@ "node_modules/tslint-microsoft-contrib" ], "rules": { - "align": false, + // Basic + "import-blacklist": [ + true, + "lodash", + "rxjs/Rx", + "rxjs" + ], + "no-multiline-string": false, + "arrow-parens": [true, "ban-single-arg-parens"], + "max-file-line-count": [true, 600], + "jsdoc-format": true, + "cyclomatic-complexity": [true, 20], + "space-before-function-paren": [ + true, + { + "anonymous": "never", + "named": "never", + "asyncArrow": "always", + "constructor": "never", + "method": "never" + } + ], + + // Microsoft + "no-relative-imports": false, + "mocha-no-side-effect-code": false, + "missing-jsdoc": false, "chai-vague-errors": false, "export-name": false, - "function-name": [true, { - "method-regex": "^[a-z][\\w\\d]+$", - "private-method-regex": "^[a-z][\\w\\d]+$", - "static-method-regex": "^[A-Z][\\w\\d]+$", - "function-regex": "^[a-z][\\w\\d]+$" - }], - "missing-jsdoc": false, - "mocha-no-side-effect-code": false, - "no-any": false, - "no-constructor-vars": false, - "no-empty-interfaces": false, - "no-invalid-this": false, - "no-multiline-string": false, - "no-require-imports": false, - "no-relative-imports": false, - "no-typeof-undefined": false, - "no-var-requires": false, - "insecure-random": false, "no-reserved-keywords": false, - "no-backbone-get-set-outside-model": false, + "no-any": false, + "no-increment-decrement": false, "trailing-comma": [true, {"multiline": "always", "singleline": "never"}], - "underscore-consistent-invocation": false + "no-typeof-undefined": false, + "underscore-consistent-invocation": false, + "no-backbone-get-set-outside-model": false, + "function-name": [true, { + "method-regex": "^[a-z][\\w\\d]+$", + "private-method-regex": "^[a-z][\\w\\d]+$", + "static-method-regex": "^[a-z][\\w\\d]+$", + "function-regex": "^[a-z][\\w\\d]+$" + }], + "typedef-whitespace": [ + true, + { + "call-signature": "nospace", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + }, + { + "call-signature": "onespace", + "index-signature": "onespace", + "parameter": "onespace", + "property-declaration": "onespace", + "variable-declaration": "onespace" + } + ], + "no-stateless-class": false, + "insecure-random": false, + "variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore"], // for unused params + + "no-suspicious-comment": false, // For the duration of pre-release development these will be fine, rm and fix on before release. + "no-string-literal": false, + "no-string-throw": true, + "no-empty-line-after-opening-brace": true, + "no-function-expression": true } }