Skip to content

Commit

Permalink
Merge branch 'c4dt:main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
MaximeZmt authored May 29, 2024
2 parents 62cf3e8 + 0ce24ab commit 7242c71
Show file tree
Hide file tree
Showing 15 changed files with 173 additions and 84 deletions.
13 changes: 10 additions & 3 deletions scripts/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Scripts for running D-voting locally

The following scripts are available to configure and run D-voting locally. They should be called in this order:
The following scripts are available to configure and run D-voting locally.
They should be called in this order:

- `run_local.sh` - sets up a complete system with 4 nodes, the db, the authentication-server,
and the frontend.
Expand All @@ -10,8 +11,14 @@ The following scripts are available to configure and run D-voting locally. They
For debugging Dela, you still need to re-run everything.
- `local_proxies.sh` needs to be run once after the `run_local.sh` script
- `local_forms.sh` creates a new form and prints its ID
- `local_votes.sh` casts the given number of votes. THE ENCRYPTION IS WRONG AND WILL NOT WORK. But it allows to test
missing votes

Every script must be called from the root of the repository:

```bash
./scripts/run_local.sh
./scripts/local_proxies.sh
./scripts/local_forms.sh
```

The following script is only called by the other scripts:

Expand Down
3 changes: 2 additions & 1 deletion scripts/local_forms.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
. "$SCRIPT_DIR/local_login.sh"

echo "add form"
RESP=$(curl -sk "$FRONTEND_URL/api/evoting/forms" -X POST -H 'Content-Type: application/json' -b cookies.txt --data-raw '{"Title":{"En":"Something","Fr":"","De":""},"Scaffold":[{"ID":"99hYV4uy","Title":{"En":"More stuff","Fr":"","De":""},"Order":["0c7RSRKs"],"Ranks":[],"Selects":[{"ID":"0c7RSRKs","Title":{"En":"Choose","Fr":"","De":""},"MaxN":3,"MinN":1,"Choices":[{"Choice":"{\"en\":\"First\"}","URL":""},{"Choice":"{\"en\":\"Second\"}","URL":""},{"Choice":"{\"en\":\"Third\"}","URL":""}],"Hint":{"En":"","Fr":"","De":""}}],"Texts":[],"Subjects":[]}]}')
FORMJSON='{"Configuration":{"Title":{"En":"title","Fr":"","De":"","URL":""},"Scaffold":[{"ID":"ozCI7gKv","Title":{"En":"subtitle","Fr":"","De":"","URL":""},"Order":["nVjQ0jMK"],"Ranks":[],"Selects":[{"ID":"nVjQ0jMK","Title":{"En":"vote","Fr":"","De":"","URL":""},"MaxN":3,"MinN":1,"Choices":[{"Choice":"{\"en\":\"one\"}","URL":""},{"Choice":"{\"en\":\"two\"}","URL":""},{"Choice":"{\"en\":\"three\"}","URL":""}],"Hint":{"En":"","Fr":"","De":""}}],"Texts":[],"Subjects":[]}],"AdditionalInfo":""}}'
RESP=$(curl -sk "$FRONTEND_URL/api/evoting/forms" -X POST -H 'Content-Type: application/json' -b cookies.txt --data-raw "$FORMJSON")
FORMID=$(echo "$RESP" | jq -r .FormID)
echo "FORMID=$FORMID" > "$SCRIPT_DIR/formid.env"

Expand Down
20 changes: 0 additions & 20 deletions scripts/local_votes.sh

This file was deleted.

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
7 changes: 0 additions & 7 deletions scripts/run_local.sh
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,6 @@ function init_nodes() {

function init_dela() {
echo "Initializing dela"
echo " Share the certificate"
for n in $(seq 2 4); do
TOKEN_ARGS=$(dvoting --config ./nodes/node-1 minogrpc token)
NODEDIR=./nodes/node-$n
dvoting --config $NODEDIR minogrpc join --address grpc://localhost:2000 $TOKEN_ARGS
done

echo " Create a new chain with the nodes"
for n in $(seq 4); do
NODEDIR=./nodes/node-$n
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}}"
}
}
8 changes: 6 additions & 2 deletions web/frontend/src/language/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@
"ballotFailure": "Une erreur est survenue lors de l'envoi de votre ballot. Merci de contacter l'administrateur du site. ",
"incompleteBallot": "Certaines réponses ne sont pas complètes.",
"selectMin": "Selectionnez {{minSelect}} {{singularPlural}}. ",
"selectMax": "Selectionnez au moins {{maxSelect}} {{singularPlural}}. ",
"selectMax": "Selectionnez au plus {{maxSelect}} {{singularPlural}}. ",
"selectBetween": "Selectionnez entre {{minSelect}} et {{maxSelect}} réponses. ",
"minSelectError": "Vous devez sélectionner au moins {{min}} {{singularPlural}}. ",
"maxSelectError": "Vous ne pouvez pas sélectionner plus de {{max}} réponses. ",
Expand Down 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}}"
}
}
9 changes: 8 additions & 1 deletion web/frontend/src/layout/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,14 @@ const App = () => {
</RequireAuth>
}
/>
<Route path={'/forms/:formId'} element={<FormShow />} />
<Route
path={'/forms/:formId'}
element={
<RequireAuth auth={['election', 'create']}>
<FormShow />
</RequireAuth>
}
/>
<Route path={'/forms/:formId/result'} element={<FormResult />} />
<Route
path={ROUTE_BALLOT_SHOW + '/:formId'}
Expand Down
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;
20 changes: 14 additions & 6 deletions web/frontend/src/pages/form/components/FormRow.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import React, { FC, useEffect, useState } from 'react';
import React, { FC, useContext, useEffect, useState } from 'react';
import { LightFormInfo } from 'types/form';
import { Link } from 'react-router-dom';
import FormStatus from './FormStatus';
import QuickAction from './QuickAction';
import { default as i18n } from 'i18next';
import { AuthContext } from '../../..';

type FormRowProps = {
form: LightFormInfo;
};

const SUBJECT_ELECTION = 'election';
const ACTION_CREATE = 'create';

const FormRow: FC<FormRowProps> = ({ form }) => {
const [titles, setTitles] = useState<any>({});
const authCtx = useContext(AuthContext);
useEffect(() => {
if (form.Title === undefined) return;
setTitles({ En: form.Title.En, Fr: form.Title.Fr, De: form.Title.De, URL: form.Title.URL });
Expand All @@ -25,14 +30,17 @@ const FormRow: FC<FormRowProps> = ({ form }) => {
formRowI18n.addResource(lang.toLowerCase(), 'form', 'title', title);
}
});
const formTitle = formRowI18n.t('title', { ns: 'form', fallbackLng: 'en' });
return (
<tr className="bg-white border-b hover:bg-gray-50 ">
<td className="px-1.5 sm:px-6 py-4 font-medium text-gray-900 whitespace-nowrap truncate">
<Link className="text-gray-700 hover:text-[#ff0000]" to={`/forms/${form.FormID}`}>
<div className="max-w-[20vw] truncate">
{formRowI18n.t('title', { ns: 'form', fallbackLng: 'en' })}
</div>
</Link>
{authCtx.isLogged && authCtx.isAllowed(SUBJECT_ELECTION, ACTION_CREATE) ? (
<Link className="text-gray-700 hover:text-[#ff0000]" to={`/forms/${form.FormID}`}>
<div className="max-w-[20vw] truncate">{formTitle}</div>
</Link>
) : (
<div className="max-w-[20vw] truncate">{formTitle}</div>
)}
</td>
<td className="px-1.5 sm:px-6 py-4">{<FormStatus status={form.Status} />}</td>
<td className="px-1.5 sm:px-6 py-4 text-right">
Expand Down
Loading

0 comments on commit 7242c71

Please sign in to comment.