Skip to content

Commit

Permalink
Merge pull request #157 from c4dt/feat_batch_voters
Browse files Browse the repository at this point in the history
Add voters in batches
  • Loading branch information
PascalinDe authored May 7, 2024
2 parents 5218665 + 6f748b2 commit 19e40f2
Show file tree
Hide file tree
Showing 9 changed files with 138 additions and 45 deletions.
2 changes: 1 addition & 1 deletion scripts/run_docker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# The script must be called from the root of the github tree, else it returns an error.
# This script currently only works on Linux due to differences in network management on Windows/macOS.

if [[ $(git rev-parse --show-toplevel) != $(pwd) ]]; then
if [[ $(git rev-parse --show-toplevel) != $(readlink -fn $(pwd)) ]]; then
echo "ERROR: This script must be started from the root of the git repo";
exit 1;
fi
Expand Down
15 changes: 15 additions & 0 deletions web/backend/src/authManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,18 @@ export async function addPolicy(userID: string, subject: string, permission: str
await authEnforcer.addPolicy(userID, subject, permission);
await authEnforcer.loadPolicy();
}

export async function addListPolicy(userIDs: string[], subject: string, permission: string) {
const promises = userIDs.map((userID) => authEnforcer.addPolicy(userID, subject, permission));
try {
await Promise.all(promises);
} catch (error) {
// At least one policy update has failed, but we need to reload ACLs anyway for the succeeding ones
await authEnforcer.loadPolicy();
throw new Error(`Failed to add policies for all users: ${error}`);
}
}

export async function assignUserPermissionToOwnElection(userID: string, ElectionID: string) {
return authEnforcer.addPolicy(userID, ElectionID, PERMISSIONS.ACTIONS.OWN);
}
Expand Down Expand Up @@ -87,6 +99,9 @@ export function setMapAuthorization(list: string[][]): Map<String, Array<String>
// the range between 100000 and 999999, an error is thrown.
export function readSCIPER(s: string): number {
const n = parseInt(s, 10);
if (Number.isNaN(n)) {
throw new Error(`${s} is not a number`);
}
if (n < 100000 || n > 999999) {
throw new Error(`SCIPER is out of range. ${n} is not between 100000 and 999999`);
}
Expand Down
47 changes: 34 additions & 13 deletions web/backend/src/controllers/users.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import express from 'express';

import { addPolicy, initEnforcer, isAuthorized, PERMISSIONS } from '../authManager';
import {
addPolicy,
addListPolicy,
initEnforcer,
isAuthorized,
PERMISSIONS,
readSCIPER,
} from '../authManager';

export const usersRouter = express.Router();

Expand All @@ -22,7 +29,7 @@ usersRouter.get('/user_rights', (req, res) => {
});

// This call (only for admins) allows an admin to add a role to a voter.
usersRouter.post('/add_role', (req, res, next) => {
usersRouter.post('/add_role', async (req, res, next) => {
if (!isAuthorized(req.session.userId, PERMISSIONS.SUBJECTS.ROLES, PERMISSIONS.ACTIONS.ADD)) {
res.status(400).send('Unauthorized - only admins allowed');
return;
Expand All @@ -34,17 +41,31 @@ usersRouter.post('/add_role', (req, res, next) => {
}
}

addPolicy(req.body.userId, req.body.subject, req.body.permission)
.then(() => {
res.set(200).send();
next();
})
.catch((e) => {
res.status(400).send(`Error while adding to roles: ${e}`);
});

// Call https://search-api.epfl.ch/api/ldap?q=228271, if the answer is
// empty then sciper unknown, otherwise add it in userDB
if ('userId' in req.body) {
try {
readSCIPER(req.body.userId);
await addPolicy(req.body.userId, req.body.subject, req.body.permission);
} catch (error) {
res.status(400).send(`Error while adding single user to roles: ${error}`);
return;
}
res.set(200).send();
next();
} else if ('userIds' in req.body) {
try {
req.body.userIds.every(readSCIPER);
await addListPolicy(req.body.userIds, req.body.subject, req.body.permission);
} catch (error) {
res.status(400).send(`Error while adding multiple users to roles: ${error}`);
return;
}
res.set(200).send();
next();
} else {
res
.status(400)
.send(`Error: at least one of 'userId' or 'userIds' must be send in the request`);
}
});

// This call (only for admins) allow an admin to remove a role to a user.
Expand Down
6 changes: 5 additions & 1 deletion web/frontend/src/language/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,10 @@
"footerBuild": "build:",
"footerBuildTime": "in:",
"voteNotVoter": "Wählen nicht erlaubt.",
"voteNotVoterDescription": "Sie sind nicht wahlberechtigt in dieser Wahl. Falls Sie denken, dass ein Fehler vorliegt, wenden Sie sich bitte an die verantwortliche Stelle."
"voteNotVoterDescription": "Sie sind nicht wahlberechtigt in dieser Wahl. Falls Sie denken, dass ein Fehler vorliegt, wenden Sie sich bitte an die verantwortliche Stelle.",
"addVotersLoading": "WählerInnen werden hinzugefügt...",
"sciperNaN": "'{{sciperStr}}' ist keine Zahl; ",
"sciperOutOfRange": "{{sciper}} ist nicht in dem erlaubten Bereich (100000-999999); ",
"invalidScipersFound": "Ungültige SCIPERs wurden gefunden. Es wurde keine Anfrage gesendet. Bitte korrigieren Sie folgende Fehler: {{sciperErrs}}"
}
}
6 changes: 5 additions & 1 deletion web/frontend/src/language/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,10 @@
"footerBuild": "build:",
"footerBuildTime": "in:",
"voteNotVoter": "Voting not allowed.",
"voteNotVoterDescription": "You are not allowed to vote in this form. If you believe this is an error, please contact the responsible of the service."
"voteNotVoterDescription": "You are not allowed to vote in this form. If you believe this is an error, please contact the responsible of the service.",
"addVotersLoading": "Adding voters...",
"sciperNaN": "'{{sciperStr}}' is not a number; ",
"sciperOutOfRange": "{{sciper}} is out of range (100000-999999); ",
"invalidScipersFound": "Invalid SCIPER numbers found. No request has been send. Please fix the following errors: {{sciperErrs}}"
}
}
6 changes: 5 additions & 1 deletion web/frontend/src/language/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,10 @@
"footerBuild": "build:",
"footerBuildTime": "en:",
"voteNotVoter": "Interdit de voter.",
"voteNotVoterDescription": "Vous n'avez pas le droit de voter dans cette élection. Si vous pensez qu'il s'agit d'une erreur, veuillez contacter le/la reponsable de service."
"voteNotVoterDescription": "Vous n'avez pas le droit de voter dans cette élection. Si vous pensez qu'il s'agit d'une erreur, veuillez contacter le/la reponsable de service.",
"addVotersLoading": "Ajout d'électeur·rice·s...",
"sciperNaN": "'{{sciperStr}}' n'est pas une chiffre; ",
"sciperOutOfRange": "{{sciper}} n'est pas dans les valeurs acceptées (100000-999999); ",
"invalidScipersFound": "Des SCIPERs invalides ont été trouvés. Aucune requête n'a été envoyée. Veuillez corriger les erreurs suivants: {{sciperErrs}}"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import { useTranslation } from 'react-i18next';
import { isManager } from './../../../../utils/auth';
import { AuthContext } from 'index';
import { useContext } from 'react';
import IndigoSpinnerIcon from '../IndigoSpinnerIcon';
import { OngoingAction } from 'types/form';

const AddVotersButton = ({ handleAddVoters, formID }) => {
const AddVotersButton = ({ handleAddVoters, formID, ongoingAction }) => {
const { t } = useTranslation();
const { authorization, isLogged } = useContext(AuthContext);

return (
return ongoingAction !== OngoingAction.AddVoters ? (
isManager(formID, authorization, isLogged) && (
<button data-testid="addVotersButton" onClick={handleAddVoters}>
<div className="whitespace-nowrap inline-flex items-center justify-center px-4 py-1 mr-2 border border-gray-300 text-sm rounded-full font-medium text-gray-700 hover:text-red-500">
Expand All @@ -17,6 +19,11 @@ const AddVotersButton = ({ handleAddVoters, formID }) => {
</div>
</button>
)
) : (
<div className="whitespace-nowrap inline-flex items-center justify-center px-4 py-1 mr-2 border border-gray-300 text-sm rounded-full font-medium text-gray-700">
<IndigoSpinnerIcon />
{t('addVotersLoading')}
</div>
);
};
export default AddVotersButton;
89 changes: 63 additions & 26 deletions web/frontend/src/pages/form/components/utils/useChangeAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const useChangeAction = (
const [showModalDelete, setShowModalDelete] = useState(false);
const [showModalAddVoters, setShowModalAddVoters] = useState(false);
const [showModalAddVotersSucccess, setShowModalAddVotersSuccess] = useState(false);
const [newVoters, setNewVoters] = useState('');
const [newVoters] = useState('');

const [userConfirmedProxySetup, setUserConfirmedProxySetup] = useState(false);
const [userConfirmedClosing, setUserConfirmedClosing] = useState(false);
Expand Down Expand Up @@ -314,35 +314,64 @@ const useChangeAction = (

useEffect(() => {
if (userConfirmedAddVoters.length > 0) {
const newUsersArray = [];
for (const sciperStr of userConfirmedAddVoters.split('\n')) {
let sciperErrs = '';

const providedScipers = userConfirmedAddVoters.split('\n');
setUserConfirmedAddVoters('');

for (const sciperStr of providedScipers) {
const sciper = parseInt(sciperStr, 10);
if (isNaN(sciper)) {
sciperErrs += t('sciperNaN', { sciperStr: sciperStr });
}
if (sciper < 100000 || sciper > 999999) {
sciperErrs += t('sciperOutOfRange', { sciper: sciper });
}
}
if (sciperErrs.length > 0) {
setTextModalError(t('invalidScipersFound', { sciperErrs: sciperErrs }));
setShowModalError(true);
return;
}
// requests to ENDPOINT_ADD_ROLE cannot be done in parallel because on the
// backend, auths are reloaded from the DB each time there is an update.
// While auths are reloaded, they cannot be checked in a predictable way.
// See isAuthorized, addPolicy, and addListPolicy in backend/src/authManager.ts
(async () => {
try {
const sciper = parseInt(sciperStr, 10);
if (sciper < 100000 || sciper > 999999) {
console.error(`SCIPER is out of range. ${sciper} is not between 100000 and 999999`);
} else {
const request = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: sciper, subject: formID, permission: 'vote' }),
};
sendFetchRequest(ENDPOINT_ADD_ROLE, request, setIsPosting)
.catch(console.error)
.then(() => {
newUsersArray.push(sciper);
setNewVoters(newUsersArray.join('\n'));
setShowModalAddVotersSuccess(true);
});
const chunkSize = 1000;
setOngoingAction(OngoingAction.AddVoters);
for (let i = 0; i < providedScipers.length; i += chunkSize) {
await sendFetchRequest(
ENDPOINT_ADD_ROLE,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userIds: providedScipers.slice(i, i + chunkSize),
subject: formID,
permission: 'vote',
}),
},
setIsPosting
);
}
} catch (e) {
console.error(`While adding voter: ${e}`);
setShowModalAddVoters(false);
}
}
setUserConfirmedAddVoters('');
setShowModalAddVoters(false);
setOngoingAction(OngoingAction.None);
})();
}
// setUserConfirmedAddVoters(false);
}, [formID, sendFetchRequest, userConfirmedAddVoters]);
}, [
formID,
sendFetchRequest,
userConfirmedAddVoters,
t,
setTextModalError,
setShowModalError,
setOngoingAction,
]);

useEffect(() => {
if (userConfirmedProxySetup) {
Expand Down Expand Up @@ -515,7 +544,11 @@ const useChangeAction = (
formID={formID}
/>
<DeleteButton handleDelete={handleDelete} formID={formID} />
<AddVotersButton handleAddVoters={handleAddVoters} formID={formID} />
<AddVotersButton
handleAddVoters={handleAddVoters}
formID={formID}
ongoingAction={ongoingAction}
/>
</>
);
case Status.Open:
Expand All @@ -535,7 +568,11 @@ const useChangeAction = (
/>
<VoteButton status={status} formID={formID} />
<DeleteButton handleDelete={handleDelete} formID={formID} />
<AddVotersButton handleAddVoters={handleAddVoters} formID={formID} />
<AddVotersButton
handleAddVoters={handleAddVoters}
formID={formID}
ongoingAction={ongoingAction}
/>
</>
);
case Status.Closed:
Expand Down
1 change: 1 addition & 0 deletions web/frontend/src/types/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const enum OngoingAction {
Decrypting,
Combining,
Canceling,
AddVoters,
}

interface FormInfo {
Expand Down

0 comments on commit 19e40f2

Please sign in to comment.