Skip to content

Commit bdd7d48

Browse files
committed
feat: limit access to piscine overviews
- only staff and C.A.T.s have access to piscine overviews - C.A.T.s can only view piscine overviews of the current year
1 parent a8fa6fa commit bdd7d48

File tree

15 files changed

+274
-10
lines changed

15 files changed

+274
-10
lines changed

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ SESSION_SECRET=enter_your_session_secret_here
33
INTRA_API_UID=enter_your_uid_here
44
INTRA_API_SECRET=enter_your_secret_here
55
INTRA_CAMPUS_ID=14
6+
INTRA_PISCINE_ASSISTANT_GROUP_ID=68

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# CodamHero v2
22
CodamHero v2 gives staff and students an overview of everything Codam.
33

4+
## Limited access
5+
Only staff members or C.A.T.s have access to (parts of) piscine overviews by design.
6+
47
## Development
58
To get started, run the folllowing:
69
```bash
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
-- CreateTable
2+
CREATE TABLE "Group" (
3+
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
4+
"name" TEXT NOT NULL
5+
);
6+
7+
-- CreateTable
8+
CREATE TABLE "GroupUser" (
9+
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
10+
"group_id" INTEGER NOT NULL,
11+
"user_id" INTEGER NOT NULL,
12+
"created_at" DATETIME NOT NULL,
13+
"updated_at" DATETIME NOT NULL,
14+
CONSTRAINT "GroupUser_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
15+
CONSTRAINT "GroupUser_group_id_fkey" FOREIGN KEY ("group_id") REFERENCES "Group" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
16+
);
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
Warnings:
3+
4+
- You are about to drop the column `created_at` on the `GroupUser` table. All the data in the column will be lost.
5+
- You are about to drop the column `updated_at` on the `GroupUser` table. All the data in the column will be lost.
6+
7+
*/
8+
-- RedefineTables
9+
PRAGMA foreign_keys=OFF;
10+
CREATE TABLE "new_GroupUser" (
11+
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
12+
"group_id" INTEGER NOT NULL,
13+
"user_id" INTEGER NOT NULL,
14+
CONSTRAINT "GroupUser_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
15+
CONSTRAINT "GroupUser_group_id_fkey" FOREIGN KEY ("group_id") REFERENCES "Group" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
16+
);
17+
INSERT INTO "new_GroupUser" ("group_id", "id", "user_id") SELECT "group_id", "id", "user_id" FROM "GroupUser";
18+
DROP TABLE "GroupUser";
19+
ALTER TABLE "new_GroupUser" RENAME TO "GroupUser";
20+
PRAGMA foreign_key_check("GroupUser");
21+
PRAGMA foreign_keys=ON;

prisma/schema.prisma

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ model User {
3838
cursus_users CursusUser[]
3939
project_users ProjectUser[]
4040
locations Location[]
41+
groups GroupUser[]
4142
}
4243

4344
model Cursus {
@@ -66,6 +67,24 @@ model CursusUser {
6667
cursus Cursus @relation(fields: [cursus_id], references: [id])
6768
}
6869

70+
model Group {
71+
id Int @id
72+
name String
73+
74+
// Relations
75+
group_users GroupUser[]
76+
}
77+
78+
model GroupUser {
79+
id Int @id
80+
group_id Int @map("group_id")
81+
user_id Int @map("user_id")
82+
83+
// Relations
84+
user User @relation(fields: [user_id], references: [id])
85+
group Group @relation(fields: [group_id], references: [id])
86+
}
87+
6988
model Project {
7089
id Int @id
7190
name String

src/env.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ export const SESSION_SECRET = process.env.SESSION_SECRET!;
33
export const INTRA_API_UID = process.env.INTRA_API_UID!;
44
export const INTRA_API_SECRET = process.env.INTRA_API_SECRET!;
55
export const CAMPUS_ID: number = parseInt(process.env.INTRA_CAMPUS_ID!);
6+
export const INTRA_PISCINE_ASSISTANT_GROUP_ID : number = parseInt(process.env.INTRA_PISCINE_ASSISTANT_GROUP_ID!);
67
export const NODE_ENV = process.env.NODE_ENV || "development";
78
export const DEV_DAYS_LIMIT: number = process.env.DEV_DAYS_LIMIT ? parseInt(process.env.DEV_DAYS_LIMIT) : 365;

src/handlers/authentication.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import OAuth2Strategy from 'passport-oauth2';
44
import { PrismaClient } from '@prisma/client';
55
import { INTRA_API_UID, INTRA_API_SECRET, URL_ORIGIN, SESSION_SECRET } from '../env';
66
import { getIntraUser, IntraUser } from '../intra/oauth';
7-
import { isStudentOrStaff } from '../utils';
7+
import { isStudentOrStaff, isCatOrStaff } from '../utils';
88

99
export const setupPassport = function(prisma: PrismaClient): void {
1010
passport.use(new OAuth2Strategy({
@@ -51,6 +51,7 @@ export const setupPassport = function(prisma: PrismaClient): void {
5151
display_name: user.display_name,
5252
kind: user.kind,
5353
isStudentOrStaff: await isStudentOrStaff(user),
54+
isCatOrStaff: await isCatOrStaff(user),
5455
image_url: user.image,
5556
};
5657
cb(null, intraUser);

src/handlers/middleware.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import express from 'express';
22
import { Request, Response, NextFunction } from "express";
33
import { CustomSessionData } from "./session";
44
import { IntraUser } from '../intra/oauth';
5+
import { hasPiscineHistoryAccess } from '../utils';
56

67

78
const checkIfAuthenticated = function(req: Request, res: Response, next: NextFunction) {
@@ -35,6 +36,28 @@ export const checkIfStudentOrStaff = async function(req: Request, res: Response,
3536
}
3637
};
3738

39+
export const checkIfCatOrStaff = async function(req: Request, res: Response, next: NextFunction) {
40+
// Warning: should be used together with checkIfStudentOrStaff
41+
if ((req.user as IntraUser)?.isCatOrStaff === true) {
42+
return next();
43+
}
44+
else {
45+
console.warn(`User ${(req.user as IntraUser)?.id} is not a C.A.T. (piscine assistant) or staff member, denying access to ${req.path}.`);
46+
res.status(403);
47+
return res.send('Forbidden');
48+
}
49+
};
50+
51+
export const checkIfPiscineHistoryAccess = async function(req: Request, res: Response, next: NextFunction) {
52+
const year = parseInt(req.params.year);
53+
if (hasPiscineHistoryAccess(req.user as IntraUser, year)) {
54+
return next();
55+
}
56+
console.warn(`User ${(req.user as IntraUser)?.id} is trying to access a piscine overview for ${year} (which is in the past), denying access to ${req.path}.`);
57+
res.status(403);
58+
return res.send('Forbidden');
59+
};
60+
3861
const expressErrorHandler = function(err: any, req: Request, res: Response, next: NextFunction) {
3962
console.error(err);
4063
res.status(500);

src/intra/base.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import { syncProjectsUsers } from "./projects_users";
77
import { syncLocations } from "./locations";
88
import { DEV_DAYS_LIMIT, NODE_ENV } from "../env";
99
import { cleanupDB } from "./cleanup";
10-
import { invalidateAllCache } from "../handlers/cache";
1110
import { buildPiscineCache } from "../handlers/piscine";
11+
import { syncGroups } from "./groups";
12+
import { syncGroupsUsers } from "./groups_users";
1213

1314
export const prisma = new PrismaClient();
1415
export const SYNC_INTERVAL = 10; // minutes
@@ -173,6 +174,8 @@ export const syncWithIntra = async function(api: Fast42): Promise<void> {
173174
await syncProjects(api, now);
174175
await syncProjectsUsers(api, now);
175176
await syncLocations(api, now);
177+
await syncGroups(api, now);
178+
await syncGroupsUsers(api, now);
176179
await cleanupDB(api);
177180

178181
// Clear the server-side cache (should not be needed because of the cache rebuilding below)

src/intra/groups.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import Fast42 from '@codam/fast42';
2+
import { prisma, syncDataCB } from './base';
3+
import { INTRA_PISCINE_ASSISTANT_GROUP_ID } from '../env';
4+
5+
export const syncGroups = async function(api: Fast42, syncDate: Date): Promise<void> {
6+
// Fetch the last synchronization date from the database
7+
const syncKind = await prisma.synchronization.findFirst({
8+
where: {
9+
kind: 'groups',
10+
},
11+
});
12+
13+
// We only sync the C.A.T. group (id defined in INTRA_PISCINE_ASSISTANT_GROUP_ID)
14+
// In the future we might want to sync all groups, but then syncing groups_users will be a pain as there is no campus filter on that endpoint.
15+
await syncDataCB(api, syncDate, syncKind?.last_synced_at, `/groups/${INTRA_PISCINE_ASSISTANT_GROUP_ID}`, {}, async (group) => {
16+
try {
17+
await prisma.group.upsert({
18+
where: {
19+
id: group.id,
20+
},
21+
update: {
22+
name: group.name,
23+
},
24+
create: {
25+
id: group.id,
26+
name: group.name,
27+
}
28+
});
29+
}
30+
catch (err) {
31+
console.error(`Error syncing group ${group.id}: ${err}`);
32+
}
33+
});
34+
35+
// Mark synchronization as complete by updating the last_synced_at field
36+
await prisma.synchronization.upsert({
37+
where: {
38+
kind: 'groups',
39+
},
40+
update: {
41+
last_synced_at: syncDate,
42+
},
43+
create: {
44+
kind: 'groups',
45+
first_synced_at: syncDate,
46+
last_synced_at: syncDate,
47+
},
48+
});
49+
};

0 commit comments

Comments
 (0)