Skip to content

Commit 6fb6ef3

Browse files
authored
Merge pull request #315 from webkom/fix-race-condition
Fix race condition and add test
2 parents bcbc692 + c59cc2d commit 6fb6ef3

File tree

9 files changed

+93
-8
lines changed

9 files changed

+93
-8
lines changed

.drone.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ pipeline:
2121
- sudo su -c "mkdir -p /root/.config/yarn/"
2222
- sudo su -c "chown -R circleci:circleci ."
2323
- sudo su -c "chown -R circleci:circleci /root/"
24-
- MONGO_URL=mongodb://mongodb:27017/vote-integration-test HEADLESS=true yarn protractor
24+
- MONGO_URL=mongodb://mongodb:27017/vote-integration-test REDIS_URL=redis HEADLESS=true yarn protractor
2525
lint:
2626
image: node:11
2727
when:
@@ -39,7 +39,7 @@ pipeline:
3939
- pull_request
4040
group: testing
4141
commands:
42-
- MONGO_URL=mongodb://mongodb:27017/vote-test yarn mocha
42+
- MONGO_URL=mongodb://mongodb:27017/vote-test REDIS_URL=redis yarn mocha
4343
build:
4444
image: node:11
4545
when:
@@ -68,3 +68,5 @@ pipeline:
6868
services:
6969
mongodb:
7070
image: mongo:3.6
71+
redis:
72+
image: redis:latest

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ $ yarn start
4343
- `MONGO_URL`
4444
- Url to the database connection
4545
- `default`: `mongodb://localhost:27017/vote`
46+
- `REDIS_URL`
47+
- Hostname of the redis server
48+
- `default`: `localhost`
4649
- `LOGO_SRC` _(optional)_
4750
- Url to the main logo on all pages
4851
- `default`: `/static/images/Abakule.jpg`

app/models/alternative.js

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ const mongoose = require('mongoose');
55
const Election = require('./election');
66
const Vote = require('./vote');
77
const errors = require('../errors');
8+
const env = require('../../env');
9+
10+
const redisClient = require('redis').createClient(6379, env.REDIS_URL);
11+
const Redlock = require('redlock');
12+
13+
const redlock = new Redlock([redisClient], {});
814

915
const Schema = mongoose.Schema;
1016

@@ -38,19 +44,30 @@ alternativeSchema.methods.addVote = async function(user) {
3844
if (user.admin) throw new errors.AdminVotingError();
3945
if (user.moderator) throw new errors.ModeratorVotingError();
4046

47+
const lock = await redlock.lock('vote:' + user.username, 2000);
4148
const election = await Election.findById(this.election).exec();
42-
if (!election.active) throw new errors.InactiveElectionError();
49+
if (!election.active) {
50+
await lock.unlock();
51+
throw new errors.InactiveElectionError();
52+
}
4353
const votedUsers = election.hasVotedUsers.toObject();
4454
const hasVoted = _.find(votedUsers, { user: user._id });
45-
if (hasVoted) throw new errors.AlreadyVotedError();
55+
if (hasVoted) {
56+
await lock.unlock();
57+
throw new errors.AlreadyVotedError();
58+
}
4659

4760
// 24 character random string
4861
const voteHash = crypto.randomBytes(12).toString('hex');
4962
const vote = new Vote({ hash: voteHash, alternative: this.id });
5063

5164
election.hasVotedUsers.push({ user: user._id });
5265
await election.save();
53-
return vote.save();
66+
const savedVote = await vote.save();
67+
68+
await lock.unlock();
69+
70+
return savedVote;
5471
};
5572

5673
module.exports = mongoose.model('Alternative', alternativeSchema);

deployment/docker-compose.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@ version: '2'
33
services:
44
mongo:
55
image: mongo:3.6
6+
redis:
7+
image: redis:latest
68
vote:
79
image: abakus/vote:latest
810
environment:
911
# Reed more about these in the README-file in the base directory
1012
MONGO_URL: 'mongodb://mongo:27017/vote'
13+
REDIS_URL: 'redis'
1114
COOKIE_SECRET: 'long-secret-here-is-important'
1215
LOGO_SRC: 'https://raw.githubusercontent.com/webkom/lego/master/assets/abakus_webkom.png'
1316
ports:

docker-compose.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,8 @@ services:
55
image: mongo:3.6
66
ports:
77
- '127.0.0.1:27017:27017'
8+
redis:
9+
image: redis:latest
10+
ports:
11+
- '127.0.0.1:6379:6379'
12+

env.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@ module.exports = {
1010
HOST: process.env.HOST || 'localhost',
1111
// DSN url for reporting errors to sentry
1212
RAVEN_DSN: process.env.RAVEN_DSN,
13-
MONGO_URL: process.env.MONGO_URL || 'mongodb://localhost:27017/vote'
13+
MONGO_URL: process.env.MONGO_URL || 'mongodb://localhost:27017/vote',
14+
REDIS_URL: process.env.REDIS_URL || 'localhost'
1415
};

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"lint:prettier": "prettier '**/*.js' --list-different --ignore-path .gitignore",
1515
"lint:yaml": "yarn yamllint .drone.yml docker-compose.yml usage.yml deployment/docker-compose.yml",
1616
"prettier": "prettier '**/*.js' --write",
17-
"mocha": "NODE_ENV=test MONGO_URL=${MONGO_URL:-'mongodb://localhost:27017/vote-test'} nyc mocha test/**/*.test.js",
17+
"mocha": "NODE_ENV=test MONGO_URL=${MONGO_URL:-'mongodb://localhost:27017/vote-test'} nyc mocha test/**/*.test.js --exit",
1818
"coverage": "nyc report --reporter=text-lcov | coveralls",
1919
"protractor": "webdriver-manager update --standalone false && NODE_ENV=protractor MONGO_URL=${MONGO_URL:-'mongodb://localhost:27017/vote-test'} protractor ./features/protractor-conf.js",
2020
"postinstall": "yarn build"
@@ -62,6 +62,8 @@
6262
"qr-scanner": "1.1.1",
6363
"qrcode": "1.3.3",
6464
"raven": "2.6.4",
65+
"redis": "2.8.0",
66+
"redlock": "3.1.2",
6567
"serve-favicon": "2.5.0",
6668
"socket.io": "2.2.0",
6769
"style-loader": "0.23.1",

test/api/vote.test.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,27 @@ describe('Vote API', () => {
136136
votes.length.should.equal(1);
137137
});
138138

139+
it('should not be vulnerable to race conditions', async function() {
140+
const create = () =>
141+
request(app)
142+
.post('/api/vote')
143+
.send(votePayload(this.activeAlternative.id));
144+
await Promise.all([
145+
create(),
146+
create(),
147+
create(),
148+
create(),
149+
create(),
150+
create(),
151+
create(),
152+
create(),
153+
create(),
154+
create()
155+
]);
156+
const votes = await Vote.find({ alternative: this.activeAlternative.id });
157+
votes.length.should.equal(1);
158+
});
159+
139160
it('should not be possible to vote without logging in', async function() {
140161
passportStub.logout();
141162
const { body: error } = await request(app)

yarn.lock

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -750,7 +750,7 @@ [email protected]:
750750
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9"
751751
integrity sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==
752752

753-
[email protected], bluebird@^3.5.3:
753+
[email protected], bluebird@^3.3.3, bluebird@^3.5.3:
754754
version "3.5.3"
755755
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.3.tgz#7d01c6f9616c9a51ab0f8c549a79dfe6ec33efa7"
756756
integrity sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw==
@@ -1781,6 +1781,11 @@ domain-browser@^1.1.1:
17811781
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
17821782
integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==
17831783

1784+
double-ended-queue@^2.1.0-0:
1785+
version "2.1.0-0"
1786+
resolved "https://registry.yarnpkg.com/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz#103d3527fd31528f40188130c841efdd78264e5c"
1787+
integrity sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw=
1788+
17841789
duplexify@^3.4.2, duplexify@^3.6.0:
17851790
version "3.6.1"
17861791
resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.6.1.tgz#b1a7a29c4abfd639585efaecce80d666b1e34125"
@@ -5042,6 +5047,32 @@ readdirp@^2.0.0:
50425047
micromatch "^3.1.10"
50435048
readable-stream "^2.0.2"
50445049

5050+
redis-commands@^1.2.0:
5051+
version "1.4.0"
5052+
resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.4.0.tgz#52f9cf99153efcce56a8f86af986bd04e988602f"
5053+
integrity sha512-cu8EF+MtkwI4DLIT0x9P8qNTLFhQD4jLfxLR0cCNkeGzs87FN6879JOJwNQR/1zD7aSYNbU0hgsV9zGY71Itvw==
5054+
5055+
redis-parser@^2.6.0:
5056+
version "2.6.0"
5057+
resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-2.6.0.tgz#52ed09dacac108f1a631c07e9b69941e7a19504b"
5058+
integrity sha1-Uu0J2srBCPGmMcB+m2mUHnoZUEs=
5059+
5060+
5061+
version "2.8.0"
5062+
resolved "https://registry.yarnpkg.com/redis/-/redis-2.8.0.tgz#202288e3f58c49f6079d97af7a10e1303ae14b02"
5063+
integrity sha512-M1OkonEQwtRmZv4tEWF2VgpG0JWJ8Fv1PhlgT5+B+uNq2cA3Rt1Yt/ryoR+vQNOQcIEgdCdfH0jr3bDpihAw1A==
5064+
dependencies:
5065+
double-ended-queue "^2.1.0-0"
5066+
redis-commands "^1.2.0"
5067+
redis-parser "^2.6.0"
5068+
5069+
5070+
version "3.1.2"
5071+
resolved "https://registry.yarnpkg.com/redlock/-/redlock-3.1.2.tgz#27c82534da8548027aa8d61084a08078ae74ed36"
5072+
integrity sha512-CKXhOvLd4In5QOfbcF0GIcSsa+pL9JPZd+eKeMk/Sydxh93NUNtwQMTIKFqwPu3PjEgDStwYFJTurVzNA33pZw==
5073+
dependencies:
5074+
bluebird "^3.3.3"
5075+
50455076
regenerate@^1.2.1:
50465077
version "1.4.0"
50475078
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11"

0 commit comments

Comments
 (0)