diff --git a/CHANGELOG.md b/CHANGELOG.md index 87c38b3..0f4d7f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,24 @@ # Radiks-Server Changelog +## 1.1.0-beta.3 - July 30, 2019 + +- Fixed an issue with validating usernames + +## 0.2.0 - July 26th, 2019 + +- All code from the `v1.0.0` betas has been made backwards compatible, so we're publishing these changes as `v0.2.0`. +- Port to Typescript (Thanks [@pradel](https://github.com/pradel)) +- Automatically reconnect to MongoDB if the connection was closed +- Fixed a bug around validating models before saving them + ## 1.0.0-beta.3 - July 26th, 2019 - Fix from @pradel around validating models before saving them. [#20](https://github.com/blockstack-radiks/radiks-server/pull/20) +## 1.1.0-beta.1 - July 23, 2019 + +- Adds server validation if `username` is included in the model, to validate ownership of that username. See [#19](https://github.com/blockstack-radiks/radiks-server/pull/19) + ## 1.0.0-beta.3 - July 22nd, 2019 - Adds configuration to automatically try reconnecting to MongoDB if the connection was destroyed @@ -16,6 +31,10 @@ - Ported existing codebase to Typescript. Thanks to [@pradel](https://github.com/blockstack-radiks/radiks-server/pull/14)! +## 0.1.13 - June 22, 2019 + +- Fixed CORS error blocking DELETE requests + ## 0.1.12 - June 9th, 2019 - Added route to return count of models for a certain query, thanks to @pradel [#9] diff --git a/package.json b/package.json index 9a5cfde..c31ff86 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "radiks-server", - "version": "1.0.0-beta.4", + "version": "1.1.0-beta.3", "description": "An express plugin for building a Radiks server", "main": "app/index.js", "types": "typed", diff --git a/src/controllers/ModelsController.ts b/src/controllers/ModelsController.ts index b6dc9eb..788507e 100644 --- a/src/controllers/ModelsController.ts +++ b/src/controllers/ModelsController.ts @@ -25,7 +25,7 @@ const makeModelsController = ( uri: gaiaURL, json: true, }); - const validator = new Validator(radiksCollection, attrs); + const validator = new Validator(radiksCollection, attrs, gaiaURL); try { await validator.validate(); await radiksCollection.save(attrs); diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 0f18bed..cb01848 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -2,6 +2,6 @@ const constants = { STREAM_CRAWL_EVENT: 'STREAM_CRAWL_EVENT', COLLECTION: 'radiks-server-data', CENTRAL_COLLECTION: 'radiks-central-data', -} +}; export default constants; diff --git a/src/lib/validator.ts b/src/lib/validator.ts index 98fd3a0..7e2379d 100644 --- a/src/lib/validator.ts +++ b/src/lib/validator.ts @@ -1,20 +1,24 @@ import { Collection } from 'mongodb'; import { verifyECDSA } from 'blockstack/lib/encryption'; +import request from 'request-promise'; const errorMessage = (message: string) => { throw new Error(`Error when validating: ${message}`); }; class Validator { - private db: Collection; + public db: Collection; - private attrs: any; + public attrs: any; - private previous: any; + public previous: any; - constructor(db: Collection, attrs: any) { + public gaiaURL?: string; + + constructor(db: Collection, attrs: any, gaiaURL?: string) { this.db = db; this.attrs = attrs; + this.gaiaURL = gaiaURL; } async validate() { @@ -22,6 +26,7 @@ class Validator { await this.fetchPrevious(); await this.validateSignature(); await this.validatePrevious(); + await this.validateUsername(); return true; } @@ -84,6 +89,55 @@ class Validator { errorMessage(`No '${key}' attribute, which is required.`); } } + + /** + * If a username is included in the model attributes, then validate that + * the model was created by the owner of the username. This is done by matching + * the Gaia URL to any Gaia URL in that user's profile.json + */ + async validateUsername(): Promise { + if (!(this.attrs.username)) { + return true; + } + if (!(this.gaiaURL)) { + return errorMessage(`No 'gaiaURL' attribute, which is required for models with usernames.`); + } + const gaiaAddresses = await this.fetchProfileGaiaAddresses(); + const gaiaAddressParts = this.gaiaURL.split('/'); + const gaiaAddress = gaiaAddressParts[gaiaAddressParts.length - 3]; + const foundUrl = gaiaAddresses.find((address) => address === gaiaAddress); + + if (!foundUrl) { + return errorMessage('Username does not match provided Gaia URL'); + } + + return true; + } + + /** + * Fetch all gaia addresses from the 'apps' object in this user's profile.json + */ + private async fetchProfileGaiaAddresses(): Promise { + const uri = `https://core.blockstack.org/v1/users/${this.attrs.username}`; + try { + const response = await request({ + uri, + json: true, + }); + const user = response[this.attrs.username]; + if (user && user.profile && user.profile.apps) { + const urls: string[] = Object.values(user.profile.apps); + return urls.map((url) => { + const parts = url.split('/'); + return parts[parts.length - 2]; + }); + } + return []; + } catch (error) { + console.error(error); + return []; + } + } } -export default Validator; \ No newline at end of file +export default Validator; diff --git a/test/mocks.ts b/test/mocks.ts index 940f35b..bebf44c 100644 --- a/test/mocks.ts +++ b/test/mocks.ts @@ -4,6 +4,8 @@ import constants from '../src/lib/constants'; const userGroupId = faker.random.uuid(); +const withUsernameId = faker.random.uuid(); + const models = { test1: { name: faker.name.findName(), @@ -38,6 +40,184 @@ const models = { signingKeyId: 'personal', _id: faker.random.uuid(), }, + withUsername: { + username: 'hankstoever.id', + gaiaURL: `https://gaia.blockstack.org/hub/1Me8MbfjnNEeK5MWGokVM6BLy9UbBf7kTD/${withUsernameId}`, + _id: withUsernameId, + }, +}; + +const users = { + 'https://core.blockstack.org/v1/users/hankstoever.id': { + 'hankstoever.id': { + owner_address: '1G8XTwZkUzu7DJYDW4oA4JX5shnW8LcpC2', + profile: { + '@context': 'http://schema.org', + '@type': 'Person', + account: [ + { + '@type': 'Account', + identifier: 'hstove', + placeholder: false, + proofType: 'http', + proofUrl: + 'https://gist.github.com/hstove/40d3d545fb1d58abfc674f1c2dd581bd', + service: 'github', + }, + { + '@type': 'Account', + identifier: 'heynky', + placeholder: false, + proofType: 'http', + proofUrl: '', + service: 'twitter', + }, + ], + api: { + gaiaHubConfig: { + url_prefix: 'https://gaia.blockstack.org/hub/', + }, + gaiaHubUrl: 'https://hub.blockstack.org', + }, + apps: { + 'http://127.0.0.1:3001': + 'https://gaia.blockstack.org/hub/1Me8MbfjnNEeK5MWGokVM6BLy9UbBf7kTD/', + 'http://humans.name': + 'https://gaia.blockstack.org/hub/1JcRkU1ooEJNBedGYortWGEuxnHuMZeax4/', + 'http://localhost:3000': + 'https://gaia.blockstack.org/hub/1Huuk9uNiyFVLcn25GVtD56oSr5JxSFNdU/', + 'http://localhost:5000': + 'https://gaia.blockstack.org/hub/18oiXHGHKiWVwCu3JxsED93ok8WMQcQYCu/', + 'http://localhost:8081': + 'https://gaia.blockstack.org/hub/1GW81UvfDKuqVJ99YsGkf8C58owtV7yUor/', + 'https://agaze.co': + 'https://gaia.blockstack.org/hub/19jniGm81USE11JMiCervUE3tQ7N1z1V5j/', + 'https://airtext.xyz': + 'https://gaia.blockstack.org/hub/145EKYwMCRQbcfrTtpwW7SXiwPwdUHxywq/', + 'https://animalkingdoms.netlify.com': + 'https://gaia.blockstack.org/hub/1EYVG8SdPdCodC3b2FLqG9wDs1YW5rQqSp/', + 'https://app.blockcred.io': + 'https://gaia.blockstack.org/hub/19J2pAqWCkxbV7Z8Y3cuRhq5a9xvHfhgNV/', + 'https://app.dmail.online': + 'https://gaia.blockstack.org/hub/1KaLFVAcv9euTvxCYLa7JvSPYaG61zXYZQ/', + 'https://app.forms.id': + 'https://gaia.blockstack.org/hub/1MKBDXzDoQ7Pu9foF7y8d96bzPPJnsoFGE/', + 'https://app.graphitedocs.com': + 'https://gaia.blockstack.org/hub/13hwRRNRXiWRvqkX4XHN8D4GPpWmWo8hGU/', + 'https://app.sigle.io': + 'https://gaia.blockstack.org/hub/16TTzjWyq8vpHwMum1DygWLagsCYyJHv2r/', + 'https://ares.hankstoever.com': + 'https://gaia.blockstack.org/hub/15aM5eEngkGbuoXRN7ih2GS6gMDjV5RHrB/', + 'https://banter-radiks-pr-17.herokuapp.com': + 'https://gaia.blockstack.org/hub/189LnpxeS6vbB2dzmWQwo9Sr55qvRwPFf1/', + 'https://banter-radiks-staging-pr-19.herokuapp.com': + 'https://gaia.blockstack.org/hub/1FU9oWG7N9ZT9xvQ7okrgkm6LxgHdjA7qz/', + 'https://banter-radiks-staging-pr-24.herokuapp.com': + 'https://gaia.blockstack.org/hub/19NXLtyRZyZP7vQQQoWn4AjQPbSopDf8As/', + 'https://banter-radiks-staging-pr-26.herokuapp.com': + 'https://gaia.blockstack.org/hub/12K8Hyd8PDDFkKCL86fioegkPijSfhobtU/', + 'https://banter-radiks-staging-pr-60.herokuapp.com': + 'https://gaia.blockstack.org/hub/1L3qP6JNwPmh3J85emWi5h8KsFwRh95WJL/', + 'https://banter-radiks.herokuapp.com': + 'https://gaia.blockstack.org/hub/1Q2xDDaD9CpxEj1TeFEqBrvuj8XYdrJYyQ/', + 'https://banter.pub': + 'https://gaia.blockstack.org/hub/1GPmRPpP4fiJazMgTg3dreDyhiHw6jRui5/', + 'https://beta.springrole.com': + 'https://gaia.blockstack.org/hub/1DE8mb8JjdDjrz6MF6ZpXRyf2HYtouegHp/', + 'https://bitcamp.blockboardapp.com': + 'https://gaia.blockstack.org/hub/1ARNRM3a3W82ix1iaHymcvR6XMJUXn24nC/', + 'https://bitpatron.co': + 'https://gaia.blockstack.org/hub/17EdDKivpgek4DxuVAgEWMKd15NfjkNWss/', + 'https://cafe-society.news': + 'https://gaia.blockstack.org/hub/16iWBLeQtWg1TXKA1yssBEvvmGmj5hRGS5/', + 'https://dapp.cryptocracy.io': + 'https://gaia.blockstack.org/hub/155fBgqn8mvaxRh7A8tvVU9jQLnQWEUmhu/', + 'https://debutapp.social': + 'https://gaia.blockstack.org/hub/1NDADqQaVJwT2b2HBm9mS3DC5mcbopZqxW/', + 'https://festive-bhabha-7e881b.netlify.com': + 'https://gaia.blockstack.org/hub/1D3FxNQhncNtmerP9uA3Kb4m4wvBzhdSjh/', + 'https://focused-rosalind-344827.netlify.com': + 'https://gaia.blockstack.org/hub/1EUWQGrBsq6pRg8JgEfWG2ank1fcm8DCDZ/', + 'https://foodrover.info': + 'https://gaia.blockstack.org/hub/1G52nxyPRDhJD8fDGMDYobHKU8Z7jLvnVV/', + 'https://hardcore-mcclintock-a6dc09.netlify.com': + 'https://gaia.blockstack.org/hub/1N2USmXSvkE3ujrBD8YUX2xsQAuN7gbJWf/', + 'https://hungry-wilson-249b01.netlify.com': + 'https://gaia.blockstack.org/hub/1Px9k2rVbpCWKgkuxr4Edxq3PjVCGfmNkX/', + 'https://justsnake.live': + 'https://gaia.blockstack.org/hub/1AZBhQ6P1tNugUdz4ivujSmaofK2SRAcVJ/', + 'https://kanstack.herokuapp.com': + 'https://gaia.blockstack.org/hub/1C4ruBsUxbZd72sPUn73BJSLsfSQwgChMh/', + 'https://lettermesh.com': + 'https://gaia.blockstack.org/hub/14Z9niGmdTnSDeL54mBVuXsaDmkzvRvyxK/', + 'https://musing-mcclintock-2e6e1a.netlify.com': + 'https://gaia.blockstack.org/hub/14pCC5W7A6G6gmcg2cTDf8Sd69cKDjv32k/', + 'https://pgeon.com': + 'https://gaia.blockstack.org/hub/14Gz68LLQcC4C8pwfZx1h6SerAL7gUhYKF/', + 'https://pressure.carolkng.com': + 'https://gaia.blockstack.org/hub/1FGrc6ZrNzfa8J2PvDE17s372gDrd9f3Gm/', + 'https://remark.cryptocracy.io': + 'https://gaia.blockstack.org/hub/14M4dLuVEbP1RssyimtP5bc3TWgixkVNBW/', + 'https://staging--objective-wiles-65a1ef.netlify.com': + 'https://gaia.blockstack.org/hub/1KBkWtMNhDnfsQ1N2Y5jaSWeJXBD3mM25d/', + 'https://staging.banter.pub': + 'https://gaia.blockstack.org/hub/15pUGtq7x7s12brbJ1GwCvNTdhUgpnEkUz/', + 'https://testnet.dmail.online': + 'https://gaia.blockstack.org/hub/1GSu4ainHjMRRGhvjGYcMw6NbHGCEvBwAq/', + 'https://www.chat.hihermes.co': + 'https://gaia.blockstack.org/hub/1DiqxTBwCrU2LDBKteaUcG2L88hYdoXuA8/', + 'https://www.justsnake.live': + 'https://gaia.blockstack.org/hub/1FuLK4cxv63pfkhxc9Sv8AMDLWCzG2jN2Z/', + 'https://www.myblockspace.com': + 'https://gaia.blockstack.org/hub/16HRkNBUaCxTj2P5foK2TAnPM1aEFXfD57/', + 'https://www.web.stealthy.im': + 'https://gaia.blockstack.org/hub/1FegzjDY9fRoQeYW6gedAFVQpgD8i6ZP6P/', + 'https://xordrive.io': + 'https://gaia.blockstack.org/hub/15MZUgQg3wqm4g3yWbft2JHL98wjrJJiHa/', + }, + description: 'Developer at Blockstack', + image: [ + { + '@type': 'ImageObject', + contentUrl: + 'https://gaia.blockstack.org/hub/1G8XTwZkUzu7DJYDW4oA4JX5shnW8LcpC2//avatar-0', + name: 'avatar', + }, + ], + name: 'Hank Stoever', + }, + public_key: + '02dbe23b383136fad995327d5274f9d168a7edbbd5f89b92afa2abc86ee40e0fba', + verifications: [ + { + identifier: 'hstove', + proof_url: + 'https://gist.github.com/hstove/40d3d545fb1d58abfc674f1c2dd581bd', + service: 'github', + valid: true, + }, + { + identifier: 'heynky', + proof_url: '', + service: 'twitter', + valid: false, + }, + ], + zone_file: { + $origin: 'hankstoever.id', + $ttl: 3600, + uri: [ + { + name: '_http._tcp', + priority: 10, + target: + 'https://gaia.blockstack.org/hub/1G8XTwZkUzu7DJYDW4oA4JX5shnW8LcpC2/profile.json', + weight: 1, + }, + ], + }, + }, + }, }; const saveAll = async () => { @@ -46,4 +226,4 @@ const saveAll = async () => { await db.collection(constants.COLLECTION).insertMany(data); }; -export { models, saveAll }; +export { models, saveAll, users }; diff --git a/test/setup.ts b/test/setup.ts index 9bc566f..7f03d0f 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -8,8 +8,11 @@ dotenv.config({ }); jest.mock('request-promise', () => options => { - const { models } = require('./mocks'); // eslint-disable-line - const { uri } = options; + const { models, users } = require('./mocks'); // eslint-disable-line + const { uri }: { uri: string } = options; + if (uri.startsWith('https://core.blockstack.org/v1/users')) { + return Promise.resolve(users[uri]); + } return Promise.resolve(models[uri]); }); diff --git a/test/signer.ts b/test/signer.ts index 09be571..42d3f24 100644 --- a/test/signer.ts +++ b/test/signer.ts @@ -1,12 +1,13 @@ import { makeECPrivateKey, getPublicKeyFromPrivate } from 'blockstack/lib/keys'; import { signECDSA } from 'blockstack/lib/encryption'; +import { Db } from 'mongodb'; import uuid from 'uuid/v4'; import constants from '../src/lib/constants'; export default class Signer { - private _id: string; - private privateKey: string; - private publicKey: string; + public _id: string; + public privateKey: string; + public publicKey: string; constructor(privateKey?: string) { this.privateKey = privateKey || makeECPrivateKey(); @@ -14,7 +15,7 @@ export default class Signer { this._id = uuid(); } - save(db) { + save(db: Db) { const { _id, privateKey, publicKey } = this; return db.collection(constants.COLLECTION).insertOne({ _id, @@ -24,7 +25,7 @@ export default class Signer { }); } - sign(doc) { + sign(doc: any) { const now = new Date().getTime(); doc.updatedAt = now; doc.signingKeyId = doc.signingKeyId || this._id; diff --git a/test/validator.test.ts b/test/validator.test.ts index 7944bb4..038d558 100644 --- a/test/validator.test.ts +++ b/test/validator.test.ts @@ -6,7 +6,7 @@ import Signer from './signer'; import constants from '../src/lib/constants'; import Validator from '../src/lib/validator'; -test('it validates new models', async () => { +test('it validates new models (not requiring a gaiaURL)', async () => { const signer = new Signer(); const db = await getDB(); await signer.save(db); @@ -145,6 +145,63 @@ test('allows users to use personal signing key', async () => { const signer = new Signer(privateKey); const db = await getDB(); signer.sign(user); - const validator = new Validator(db.collection(constants.COLLECTION), user); + const validator = new Validator( + db.collection(constants.COLLECTION), + user, + `https://gaia.blockstack.org/hub/1Me8MbfjnNEeK5MWGokVM6BLy9UbBf7kTD/User/${user.username}` + ); + expect(await validator.validate()).toEqual(true); +}); + +test('throws if username included and gaia URL not found in profile', async () => { + const model = { + ...models.withUsername, + }; + model.gaiaURL = `https://gaia.blockstack.org/hub/1Me8MbfjnNEeK5MWGokVM6BLy9UbBf7kTF/ModelName/${model._id}`; + const signer = new Signer(); + const db = await getDB(); + signer.sign(model); + await signer.save(db); + await db.collection(constants.COLLECTION).insertOne(model); + const validator = new Validator( + db.collection(constants.COLLECTION), + model, + model.gaiaURL + ); + await expect(validator.validate()).rejects.toThrow( + 'Username does not match provided Gaia URL' + ); +}); + +test('throws if username included and gaia URL not provided', async () => { + const model = { + ...models.withUsername, + }; + const signer = new Signer(); + const db = await getDB(); + signer.sign(model); + await signer.save(db); + await db.collection(constants.COLLECTION).insertOne(model); + const validator = new Validator(db.collection(constants.COLLECTION), model); + await expect(validator.validate()).rejects.toThrow( + `No 'gaiaURL' attribute, which is required for models with usernames.` + ); +}); + +test('is valid if username included and gaia URL is found in profile', async () => { + const model = { + ...models.withUsername, + }; + model.gaiaURL = `https://gaia.blockstack.org/hub/1Me8MbfjnNEeK5MWGokVM6BLy9UbBf7kTD/ModelName/${model._id}`; + const signer = new Signer(); + const db = await getDB(); + signer.sign(model); + await signer.save(db); + await db.collection(constants.COLLECTION).insertOne(model); + const validator = new Validator( + db.collection(constants.COLLECTION), + model, + model.gaiaURL + ); expect(await validator.validate()).toEqual(true); });