Skip to content

Commit 8e4d658

Browse files
committed
feat(authenticateForVotingEvent): authenticateForVotingEvent API added
The authenticateForVotingEvent allows to authenticate a user who wants to access the various stages of a voting event which are protected by authentication. It uses the authenticate API already present and adds checks for roles and the possibility to set a password the first time a users logs in
1 parent 0f01860 commit 8e4d658

File tree

5 files changed

+201
-9
lines changed

5 files changed

+201
-9
lines changed

src/api/authentication-api.ts

+56-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { map, tap, mergeMap, last, catchError } from 'rxjs/operators';
1+
import { map, tap, mergeMap, last, catchError, concatMap, toArray } from 'rxjs/operators';
22
import { Collection } from 'mongodb';
33

4-
import { findObs } from 'observable-mongo';
4+
import { findObs, updateOneObs } from 'observable-mongo';
55
import { ERRORS } from './errors';
66
import { logDebug, logError } from '../lib/utils';
7-
import { validatePasswordAgainstHash$, generateJwt$, verifyJwt } from '../lib/observables';
8-
import { EmptyError } from 'rxjs';
7+
import { validatePasswordAgainstHash$, generateJwt$, verifyJwt, getPasswordHash$ } from '../lib/observables';
8+
import { EmptyError, forkJoin } from 'rxjs';
9+
import { groupBy } from 'lodash';
910

1011
export function authenticate(usersColl: Collection<any>, credentials: { user: string; pwd: string }) {
1112
const _user = { user: credentials.user };
@@ -49,3 +50,54 @@ export function validateRequestAuthentication(headers: any) {
4950
}
5051
}
5152
}
53+
54+
export function authenticateForVotingEvent(
55+
usersColl: Collection<any>,
56+
params: { user: string; pwd: string; role: string; votingEventId: string },
57+
) {
58+
const _user = { user: params.user };
59+
return findObs(usersColl, _user).pipe(
60+
toArray(),
61+
tap(foundUsers => {
62+
if (foundUsers.length === 0) {
63+
throw ERRORS.userUnknown;
64+
}
65+
if (foundUsers.length > 1) {
66+
throw new Error(`More than one user with the same user id "${_user}"`);
67+
}
68+
if (params.role && !foundUsers[0].roles.find(r => r === params.role)) {
69+
throw ERRORS.userWithNotTheReuqestedRole;
70+
}
71+
}),
72+
concatMap(([foundUser]) => {
73+
return foundUser.pwd
74+
? authenticate(usersColl, params).pipe(map(token => ({ token, pwdInserted: false })))
75+
: // if the pwd is not found as hash in the db, it means that this is the first time the user tries to login
76+
// in this case we hash it, store it in the db
77+
getPasswordHash$(params.pwd).pipe(
78+
tap(hash => (foundUser.pwd = hash)),
79+
concatMap(() => updateOneObs({ _id: foundUser._id }, { pwd: foundUser.pwd }, usersColl)),
80+
concatMap(() =>
81+
authenticate(usersColl, params).pipe(
82+
map(token => {
83+
return { token, pwdInserted: true };
84+
}),
85+
),
86+
),
87+
);
88+
}),
89+
);
90+
}
91+
92+
export function addUsersWithRole(usersColl: Collection<any>, users: { user: string; role: string }[]) {
93+
const usersGroupedByRoles = groupBy(users, 'user');
94+
const usersWithRoles = Object.keys(usersGroupedByRoles).map(user => {
95+
const roles = usersGroupedByRoles[user].map(item => item.role);
96+
return { user, roles };
97+
});
98+
return forkJoin(usersWithRoles.map(user => updateOneObs({ user: user.user }, user, usersColl, { upsert: true })));
99+
}
100+
// export function addUserWithRole(usersColl: Collection<any>, user: { user: string; role: string }) {
101+
// const dataToUpdate = { $push: { roles: user.role }, $set: { user: user.user } };
102+
// return updateOneObs({ user: user.user }, dataToUpdate, usersColl, { upsert: true });
103+
// }

src/api/errors.ts

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export const ERRORS = {
2525
voteAlreadyPresent: { errorCode: 'V-01', mongoErrorCode: 11000, message: `vote already present` } as MongoError,
2626
pwdInvalid: { errorCode: 'A-01', message: `password not valid` },
2727
userUnknown: { errorCode: 'A-02', message: `user not known` },
28+
userWithNotTheReuqestedRole: { errorCode: 'A-03', message: `user does not have the requesated role` },
2829
technologyAlreadyPresent: {
2930
errorCode: 'V-T-01',
3031
mongoErrorCode: 11000,

src/api/service.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ import {
6161

6262
import { executeTwBlipsCollection, findLatestEdition } from './tw-blips-collection-api';
6363
import { getConfiguration } from './configuration-apis';
64-
import { authenticate } from './authentication-api';
64+
import { authenticate, authenticateForVotingEvent } from './authentication-api';
6565
import { saveLog } from './client-log-apis';
6666

6767
import { defaultTWTechnologies } from '../model/technologies.local-data';
@@ -111,6 +111,7 @@ export function isServiceKnown(service: ServiceNames) {
111111
service === ServiceNames.closeForRevote ||
112112
service === ServiceNames.getConfiguration ||
113113
service === ServiceNames.authenticate ||
114+
service === ServiceNames.authenticateForVotingEvent ||
114115
service === ServiceNames.saveLogInfo
115116
);
116117
}
@@ -255,6 +256,8 @@ function executeMongoService(
255256
returnedObservable = getConfiguration(configurationColl, serviceData);
256257
} else if (service === ServiceNames.authenticate) {
257258
returnedObservable = authenticate(usersColl, serviceData);
259+
} else if (service === ServiceNames.authenticateForVotingEvent) {
260+
returnedObservable = authenticateForVotingEvent(usersColl, serviceData);
258261
} else if (service === ServiceNames.saveLogInfo) {
259262
returnedObservable = saveLog(logColl, serviceData, ipAddress);
260263
} else {

src/mongodb/authentication-api.spec.ts

+139-4
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
import { expect } from 'chai';
22

3-
import { switchMap, map, tap, catchError, mergeMap, toArray } from 'rxjs/operators';
3+
import { switchMap, map, tap, catchError, mergeMap, toArray, concatMap } from 'rxjs/operators';
44

55
import { CachedDB, mongodbService } from '../api/service';
66
import { config } from '../api/config';
7-
import { connectObs, dropObs, insertManyObs } from 'observable-mongo';
7+
import { connectObs, dropObs, insertManyObs, deleteObs } from 'observable-mongo';
88
import { ServiceNames } from '../service-names';
99
import { ERRORS } from '../api/errors';
10-
import { of, from } from 'rxjs';
10+
import { of, from, forkJoin } from 'rxjs';
1111
import { getPasswordHash$ } from '../lib/observables';
1212
import { Collection } from 'mongodb';
13+
import { addUsersWithRole } from '../api/authentication-api';
1314

14-
describe('Authentication operations', () => {
15+
describe('1.0 - Authentication operations', () => {
1516
it('loads the users collection and then authenticates one valid user', done => {
1617
const USERS = [{ user: 'abc', pwd: 'cde' }, { user: '123', pwd: '456' }];
1718
const validCredentials = USERS[0];
@@ -71,3 +72,137 @@ describe('Authentication operations', () => {
7172
export function laodUsers(usersColl: Collection<any>, users: any[]) {
7273
return dropObs(usersColl).pipe(switchMap(() => insertManyObs(users, usersColl)));
7374
}
75+
76+
describe('1.1 - Voting Event Authentication operations', () => {
77+
it('load some users from file with no pwd specified and then authenticate some of them', done => {
78+
const VOTING_EVENT_USERS = [
79+
{ user: 'Mary', role: 'architect' },
80+
{ user: 'Mary', role: 'admin' },
81+
{ user: 'John', role: 'architect' },
82+
];
83+
84+
const cachedDb: CachedDB = { dbName: config.dbname, client: null, db: null };
85+
86+
let _client;
87+
let _userColl;
88+
const votingEventName = 'event to test login of users';
89+
let votingEventId: string;
90+
91+
const firstTimePwd = 'I am the password used for the first login';
92+
let errorMissingRoleEncountered = false;
93+
let errorWrongPwdEncountered = false;
94+
let errorUserUnknownEncountered = false;
95+
96+
connectObs(config.mongoUri)
97+
// clean the test data
98+
.pipe(
99+
tap(client => {
100+
_client = client;
101+
_userColl = client.db(config.dbname).collection(config.usersCollection);
102+
}),
103+
concatMap(() => forkJoin(VOTING_EVENT_USERS.map(user => deleteObs({ user: user.user }, _userColl)))),
104+
concatMap(() => addUsersWithRole(_userColl, VOTING_EVENT_USERS)),
105+
concatMap(() =>
106+
mongodbService(cachedDb, ServiceNames.getVotingEvents).pipe(
107+
map(votingEvents => votingEvents.filter(ve => ve.name === votingEventName)),
108+
concatMap(votingEvents => {
109+
const votingEventsDeleteObs = votingEvents.map(ve =>
110+
mongodbService(cachedDb, ServiceNames.cancelVotingEvent, { _id: ve._id, hard: true }),
111+
);
112+
return votingEvents.length > 0 ? forkJoin(votingEventsDeleteObs) : of(null);
113+
}),
114+
),
115+
),
116+
concatMap(() => mongodbService(cachedDb, ServiceNames.createVotingEvent, { name: votingEventName })),
117+
tap(id => (votingEventId = id.toHexString())),
118+
)
119+
// run the real test logic
120+
.pipe(
121+
// I do the first login - no password yet set
122+
concatMap(() => {
123+
const user = VOTING_EVENT_USERS[0].user;
124+
return mongodbService(cachedDb, ServiceNames.authenticateForVotingEvent, {
125+
user,
126+
pwd: firstTimePwd,
127+
votingEventId,
128+
});
129+
}),
130+
tap(({ token, pwdInserted }) => {
131+
expect(token).to.be.not.undefined;
132+
expect(pwdInserted).to.be.true;
133+
}),
134+
// I do a second login - the password has been already set and enchripted
135+
concatMap(() => {
136+
const user = VOTING_EVENT_USERS[0].user;
137+
return mongodbService(cachedDb, ServiceNames.authenticateForVotingEvent, {
138+
user,
139+
pwd: firstTimePwd,
140+
votingEventId,
141+
});
142+
}),
143+
tap(({ token, pwdInserted }) => {
144+
expect(token).to.be.not.undefined;
145+
expect(pwdInserted).to.be.false;
146+
}),
147+
// I do a login requesting a role I do not have
148+
concatMap(() => {
149+
const user = VOTING_EVENT_USERS[0].user;
150+
return mongodbService(cachedDb, ServiceNames.authenticateForVotingEvent, {
151+
user,
152+
pwd: firstTimePwd,
153+
role: 'the boss',
154+
votingEventId,
155+
});
156+
}),
157+
catchError(err => {
158+
errorMissingRoleEncountered = true;
159+
expect(err).to.equal(ERRORS.userWithNotTheReuqestedRole);
160+
return of(null);
161+
}),
162+
// I do a login with a wrong pwd
163+
concatMap(() => {
164+
const user = VOTING_EVENT_USERS[0].user;
165+
return mongodbService(cachedDb, ServiceNames.authenticateForVotingEvent, {
166+
user,
167+
pwd: 'wrong pwd',
168+
votingEventId,
169+
});
170+
}),
171+
catchError(err => {
172+
errorWrongPwdEncountered = true;
173+
expect(err).to.equal(ERRORS.pwdInvalid);
174+
return of(null);
175+
}),
176+
// I do a login with a no existing user
177+
concatMap(() => {
178+
return mongodbService(cachedDb, ServiceNames.authenticateForVotingEvent, {
179+
user: 'I do not exist',
180+
pwd: 'pwd',
181+
votingEventId,
182+
});
183+
}),
184+
catchError(err => {
185+
errorUserUnknownEncountered = true;
186+
expect(err).to.equal(ERRORS.userUnknown);
187+
return of(err);
188+
}),
189+
)
190+
.subscribe(
191+
() => {},
192+
err => {
193+
console.error(err);
194+
cachedDb.client.close();
195+
_client.close();
196+
done(err);
197+
},
198+
() => {
199+
expect(errorMissingRoleEncountered).to.be.true;
200+
expect(errorWrongPwdEncountered).to.be.true;
201+
expect(errorUserUnknownEncountered).to.be.true;
202+
cachedDb.client.close();
203+
_client.close();
204+
done();
205+
},
206+
);
207+
}).timeout(10000);
208+
});

src/service-names.ts

+1
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,6 @@ export enum ServiceNames {
3737
closeForRevote,
3838
getConfiguration,
3939
authenticate,
40+
authenticateForVotingEvent,
4041
saveLogInfo,
4142
}

0 commit comments

Comments
 (0)