Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor: split code into modules #343

Merged
merged 21 commits into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
617 changes: 14 additions & 603 deletions web/backend/src/Server.ts

Large diffs are not rendered by default.

85 changes: 85 additions & 0 deletions web/backend/src/authManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { SequelizeAdapter } from 'casbin-sequelize-adapter';
import { Enforcer, newEnforcer } from 'casbin';

export const PERMISSIONS = {
SUBJECTS: {
ROLES: 'roles',
PROXIES: 'proxies',
ELECTION: 'election',
},
ACTIONS: {
LIST: 'list',
REMOVE: 'remove',
ADD: 'add',
PUT: 'put',
POST: 'post',
DELETE: 'delete',
OWN: 'own',
CREATE: 'create',
VOTE: 'vote',
},
};

let authEnforcer: Enforcer;
/*
We use the postgres adapter to store the Casbin policies
we initialize the adapter with the connection string and the migrate option
the connection string has the following format:
postgres://username:password@host:port/database
the migrate option is used to create the tables if they don't exist, we set it to false because we create the tables manually
*/
async function initEnforcer() {
const dbAdapter = await SequelizeAdapter.newAdapter({
dialect: 'postgres',
host: process.env.DATABASE_HOST,
port: parseInt(process.env.DATABASE_PORT || '5432', 10),
username: process.env.DATABASE_USERNAME,
password: process.env.DATABASE_PASSWORD,
database: 'casbin',
});
return newEnforcer('src/model.conf', dbAdapter);
}

Promise.all([initEnforcer()]).then((createdEnforcer) => {
[authEnforcer] = createdEnforcer;
});

export function isAuthorized(sciper: number | undefined, subject: string, action: string): boolean {
return authEnforcer.enforceSync(sciper, subject, action);
}

export async function getUserPermissions(userID: number) {
let permissions: string[][] = [];
await authEnforcer.getFilteredPolicy(0, String(userID)).then((authRights) => {
permissions = authRights;
});
console.log(`[getUserPermissions] user has permissions: ${permissions}`);
return permissions;
}

export function assignUserPermissionToOwnElection(userID: string, ElectionID: string) {
authEnforcer.addPolicy(userID, ElectionID, PERMISSIONS.ACTIONS.OWN);
}

export function revokeUserPermissionToOwnElection(userID: string, ElectionID: string) {
authEnforcer.removePolicy(userID, ElectionID, PERMISSIONS.ACTIONS.OWN);
}

// This function helps us convert the double list of the authorization
// returned by the casbin function getFilteredPolicy to a map that link
// an object to the action authorized
// list[0] contains the policies so list[i][0] is the sciper
// list[i][1] is the subject and list[i][2] is the action
export function setMapAuthorization(list: string[][]): Map<String, Array<String>> {
const userRights = new Map<String, Array<String>>();
for (let i = 0; i < list.length; i += 1) {
const subject = list[i][1];
const action = list[i][2];
if (userRights.has(subject)) {
userRights.get(subject)?.push(action);
} else {
userRights.set(subject, [action]);
}
}
return userRights;
}
100 changes: 100 additions & 0 deletions web/backend/src/controllers/authentication.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import express from 'express';
import axios, { AxiosError } from 'axios';
import { sciper2sess } from '../session';
import { getUserPermissions, setMapAuthorization } from '../authManager';

export const authenticationRouter = express.Router();

// This is via this endpoint that the client request the tequila key, this key
// will then be used for redirection on the tequila server
authenticationRouter.get('/get_teq_key', (req, res) => {
axios
.get(`https://tequila.epfl.ch/cgi-bin/tequila/createrequest`, {
params: {
urlaccess: `${process.env.FRONT_END_URL}/api/control_key`,
service: 'Evoting',
request: 'name,firstname,email,uniqueid,allunits',
},
})
.then((response) => {
console.info(`[tequila Key] Received response from tequila: ${response.data}`);
const key = response.data.split('\n')[0].split('=')[1];
const url = `https://tequila.epfl.ch/cgi-bin/tequila/requestauth?requestkey=${key}`;
res.json({ url: url });
})
.catch((error: AxiosError) => {
console.log('message:', error.message);
res.status(500).send(`failed to request Tequila authentication: ${error.message}`);
});
});

// Here the client will send the key he/she received from the tequila, it is
// then verified on the tequila. If the key is valid, the user is then logged
// in the website through this backend
authenticationRouter.get('/control_key', (req, res) => {
const userKey = req.query.key;
const body = `key=${userKey}`;

axios
.post('https://tequila.epfl.ch/cgi-bin/tequila/fetchattributes', body)
.then((response) => {
if (!response.data.includes('status=ok')) {
throw new Error('Login did not work');
}

const sciper = response.data.split('uniqueid=')[1].split('\n')[0];
const lastname = response.data.split('\nname=')[1].split('\n')[0];
const firstname = response.data.split('\nfirstname=')[1].split('\n')[0];

req.session.userId = parseInt(sciper, 10);
req.session.lastName = lastname;
req.session.firstName = firstname;

const sciperSessions = sciper2sess.get(req.session.userId) || new Set<string>();
sciperSessions.add(req.sessionID);
sciper2sess.set(sciper, sciperSessions);

res.redirect('/logged');
})
.catch((error) => {
res.status(500).send('Login did not work');
console.log(error);
});
});

// This endpoint serves to log out from the app by clearing the session.
authenticationRouter.post('/logout', (req, res) => {
if (req.session.userId === undefined) {
res.status(400).send('not logged in');
}

const { userId } = req.session;

req.session.destroy(() => {
const a = sciper2sess.get(userId as number);
if (a !== undefined) {
a.delete(req.sessionID);
sciper2sess.set(userId as number, a);
}
res.redirect('/');
});
});

// As the user is logged on the app via this express but must also
// be logged into react. This endpoint serves to send to the client (actually to react)
// the information of the current user.
authenticationRouter.get('/personal_info', async (req, res) => {
if (!req.session.userId) {
res.status(401).send('Unauthenticated');
return;
}
const userPermissions = await getUserPermissions(req.session.userId);
res.set('Access-Control-Allow-Origin', '*');
res.json({
sciper: req.session.userId,
lastName: req.session.lastName,
firstName: req.session.firstName,
isLoggedIn: true,
authorization: Object.fromEntries(setMapAuthorization(userPermissions)),
});
});
Loading