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 permissions #35

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
110 changes: 110 additions & 0 deletions docs/doc_developers/api/permissions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Permissions

Cette page traite à la fois des permissions des utilisateurs, et de celles des applications utilisant l'API.

Tous les termes spécifiques aux permissions seront en _italique_, et leur définition peut être retrouvée dans la partie [terminologie](#terminologie).

## Terminologie

### Permission

Une _permission_ est une autorisation de réaliser une action ou d'accéder à des données. Dès que quelque chose ne devrait pas être accessible / faisable avec n'importe quelle _clé API_, une _permission_ pour faire cette dite chose doit exister.

Les _permissions_ sont divisées en 2 catégories : les _user permissions_ et les _API permissions_.

**Exemples :** La permission permettant de voir les commentaires des UEs, la permission permettant de modifier les permissions des autres, ...

### User permission

Une _user permission_ est un type de _permission_. Ces _permissions_ sont les _permissions_ liées à un utilisateur.

**Exemples :** accéder aux données privées des utilisateurs, modifier les données d'un utilisateur, ...

### API permission
Une _API permission_ est un type de _permission_. Ces _permissions_ sont les _permissions_ générales, qui portent sur toute l'API.

**Exemples :** modérer les commentaires, modérer les annales, etc...

### Application

Une application est un logiciel ayant besoin d'un accès à l'API de EtuUTT. Chaque application est reliée à un utilisateur, qui est l'administrateur de celle-ci.

**Exemples :** le front de EtuUTT, l'application EtuUTT, le site de l'intégration, ...

### Clé API (ou Api Key)

Une _clé API_ (ou _Api Key_) est une relation entre un utilisateur et une _application_. Un utilisateur ne peut avoir qu'une _clé API_ par _application_.

```{note}
Une _clé API_ **n'est pas** un token, c'est plutôt un objet qui servira à générer un token et authentifier les requêtes.

Un utilisateur n'a pas nécessairement les mêmes droits sur les différentes _applications_. Il est tout de même important de noter que rien ne l'empêchera d'utiliser une _clé API_ sur une _application_ qui n'est pas liée à cette _clé API_. Il est donc important **d'avoir confiance** en l'utilisateur, et pas uniquement en l'application.
```

**Exemple :** prenons l'exemple de l'intégration : ils auront :
* Une _clé API_ pour le pour se connecter au front de EtuUTT avec le compte `[email protected]` (reliée à l'_application_ `EtuUTT-front`)
* Une _clé API_ pour le back de leur site web (reliée à `Integration-website`)
* Une _clé API_ par utilisateur de leur application (qui n'utiliserait pas le backend de leur site web), avec uniquement les droits de base, pour leur application (reliées à `Integration-app`). Chaque _clé API_ a des permissions différentes, ce qui signifie qu'on peut donner des droits à un utilisateur en particulier sur l'_application_ de l'intégration.

### Bearer token

Le _bearer token_ est une chaîne de caractère encodant une certaine _clé API_, en utilisant le standard JWT.

### Soft grant

Un _soft grant_ ne peut se faire que sur des _user permissions_ (ça n'aurait pas de sens sur des _api permissions_).

Les _soft grant_ ne donne pas la permission à la _clé API_ sur tous les utilisateurs. Chaque utilisateur doit explicitement donner son consentement pour que la _clé API_ puisse exercer sa _permission_ sur son compte.

Une _clé API_ peut se soft grant n'importe quelle _user permission_. Tant qu'elle n'aura reçu le consentement de personne, elle n'aura aucun droit supplémentaire.

**Exemple :** Guillaume, grand rageux qu'il est, décide de développer une application, qui permet d'avoir une interface bien plus agréable que celle de EtuUTT. Il a aussi une API (en Rust, on se respecte), qui s'occupe de faire l'interface entre l'API EtuUTT et son application. Guillaume pourra donner la _permission_ à sa clé API de voir le détail des utilisateurs. Cependant, ce sera un _soft grant_, ce qui signifie qu'il n'aura au début accès aux détails d'aucun utilisateur. Teddy va alors être curieux du projet, et se connecter à son application. Pendant l'authentification avec EtuUTT, il devra donner son consentement pour que Guillaume puisse récupérer ses informations personnelles. À partir de ce moment là, Guillaume pourra utiliser sa permission sur Teddy, mais **uniquement** sur Teddy, jusqu'à ce qu'un autre utilisateur lui donne son consentement. (Ah, au fait, Teddy a pas aimé l'application et a revoke son consentement 😔)

### Hard grant

Un _hard grant_ peut se faire sur n'importe quel type de _permissions_ (_user permissions_ et _API permissions_).

Un _hard grant_ ne nécessite le consentement de personne, et s'applique sur tous les utilisateurs. Une _clé API_ ne peut évidemment pas se _hard grant_ des _permissions_.

Une _API permissions_ est nécessairement _hard granted_ (aucun sens de les _soft grant_).

**Exemple :** Guillaume rêve de pouvoir. Et finalement, il a amélioré son application (Teddy est revenu sur son choix). Son code est devenu propriété de l'UNG (merci Guillaume). Nous pouvons donc donner la _permission_ pour voir les informations personnelles des utilisateurs à l'application. Un administrateur va alors _hard grant_ la permission à Guillaume. Les utilisateurs n'ont pas besoin de donner leur consentement, Guillaume aspire tout 😈.

```{warning}
Attenation cependant à bien respecter le RGPD en faisant un _hard grant_ d'une _user permission_ ! \
À ce jour, nous ne pensons qu'à 2 _applications_ qui devraient en avoir besoin : le site de EtuUTT, et son application.
```

## Tables

Faisons un tour d'horizon des tables :
- `Application` : représente une _application_.
- `ApiKey` : représente une _clé API_. L'`ApiKey` contient un token, qui sera signé pour créer le Bearer token.
- `User` : représente un utilisateur (rien de particulier à signaler ici, la table ressemble à ce dont vous pouvez vous attendre d'une table utilisateur)
- `ApiKeyPermission` : Une _permission_ spécifique donnée à une certaine _clé API_. Cette _permission_ peut soit être _soft granted_ soit être _hard granted_.
- `GrantedPermissions` : Cette table contient les permissions données par un certain utilisateur à une certaine clé API.
- `Permission` : une _enum_ listant l'entièreté des _permissions_ prises en charge par l'API. Les _API permissions_ commencent par `API_`, et les _user permissions_ commencent par `USER_`.

## Authentification des requêtes

On va traiter l'authentification des requêtes avant la connexion, le _flow_ me paraît plus logique dans ce sens là 🙂

Pour authentifier les requêtes, on utilise un _bearer token_ (token JWT), passé dans le _header_ `Authorization`, sous le format `Bearer {token}`. Une fois décodé, le token renvoit un objet contenant un champ `token`. Ce champ permet de trouver une `ApiKey` unique. À partir de cette `ApiKey`, il est ainsi possible d'obtenir l'utilisateur authentifié, et les routes ou informations auxquelles l'utilisateur a le droit d'accéder.

## Connexion

On fera la différence entre un utilisateur et une _application_. Mais comme vous avez dû le comprendre, un utilisateur n'est rien d'autre que l'_application_ du site web de EtuUTT essayant de se connecter en tant que cet utilisateur.

La méthode de connexion "utilisateur" permettra donc de générer un _bearer token_ temporaire, avec une connexion standard (décentralisée, nom d'utilisateur / mot de passe).

La méthode de connexion "application" permettra de générer un _bearer token_ avec une durée de vie possiblement infinie (en fonction de ce que veut l'utilisateur). On passe ici par une autre application (le site EtuUTT) pour générer un _bearer token_.

### Pour un utilisateur

Pour un utilisateur, on passe par le CAS de l'UTT, avec la route `POST /auth/signin`, puis l'API nous renvoit un token pour authentifier nos requêtes, voir la partie (Authentification des requêtes)[#authentification-des-requetes]

### Pour une application

Pour une application, on génère un token pour la _clé API_ demandée, puis on retourne le _bearer token_ associé. Il faut aussi bien sauvegarder la date de dernière mise à jour (`tokenUpdatedAt`), et utiliser cette date pour toujours retourner la même version du token (champ `iat` dans l'objet à encoder avec JWT).

L'utilisateur peut renouveler les token de ses `ApiKey`. Le token sera alors modifié, pour empêcher l'accès avec l'ancien token.
149 changes: 89 additions & 60 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,50 @@ datasource db {
url = env("DATABASE_URL")
}

model ApiApplication {
id String @id @default(uuid())
name String
userId String

user User @relation(fields: [userId], references: [id])
apiKeys ApiKey[]
}

model ApiKey {
id String @id @default(uuid())
token String @unique
tokenUpdatedAt DateTime
userId String
applicationId String

user User @relation(fields: [userId], references: [id], onDelete: Cascade)
application ApiApplication @relation(fields: [applicationId], references: [id], onDelete: Cascade)
apiKeyPermissions ApiKeyPermission[]
}

model ApiKeyPermission {
id String @id @default(uuid())
permission Permission
apiKeyId String
soft Boolean

apiKey ApiKey @relation(fields: [apiKeyId], references: [id], onDelete: Cascade)
grants ApiGrantedPermission[]

@@unique([apiKeyId, permission])
}

model ApiGrantedPermission {
apiKeyPermissionId String
userId String
createdAt DateTime @default(now())

apiKeyPermission ApiKeyPermission @relation(fields: [apiKeyPermissionId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)

@@id([userId, apiKeyPermissionId])
}

model Asso {
id String @id @default(uuid())
login String @unique @db.VarChar(50)
Expand All @@ -21,12 +65,12 @@ model Asso {
descriptionShortTranslationId String @unique
descriptionTranslationId String @unique

descriptionShortTranslation Translation @relation(name: "descriptionShortTranslation", fields: [descriptionShortTranslationId], references: [id], onDelete: Cascade)
descriptionTranslation Translation @relation(name: "descriptionTranslation", fields: [descriptionTranslationId], references: [id], onDelete: Cascade)
descriptionShortTranslation Translation @relation(name: "descriptionShortTranslation", fields: [descriptionShortTranslationId], references: [id], onDelete: Cascade)
descriptionTranslation Translation @relation(name: "descriptionTranslation", fields: [descriptionTranslationId], references: [id], onDelete: Cascade)
assoMemberships AssoMembership[]
assoMessages AssoMessage[]
events Event[]
assoMembershipRoles AssoMembershipRole[]
assoMembershipRoles AssoMembershipRole[]
}

model AssoMembership {
Expand Down Expand Up @@ -55,10 +99,10 @@ model AssoMembershipRole {
name String
position Int
isPresident Boolean
assoId String
assoId String

assoMembership AssoMembership[]
asso Asso @relation(fields: [assoId], references: [id])
asso Asso @relation(fields: [assoId], references: [id])
}

model AssoMessage {
Expand Down Expand Up @@ -196,8 +240,6 @@ model Translation {
assoMessageTitleBody AssoMessage? @relation("bodyTranslation")
eventDescription Event? @relation("descriptionTranslation")
eventTitle Event? @relation("titleTranslation")
userPermissionName UserPermission? @relation("userPermissionName")
userPermissionDescription UserPermission? @relation("userPermissionDescription")
annalReportReasonDescriptions UeAnnalReportReason?
commentReportReasonDescriptions UeCommentReportReason?
starCriterionDescriptions UeStarCriterion?
Expand Down Expand Up @@ -455,7 +497,7 @@ model UeInfo {

model UeStarCriterion {
id String @id @default(uuid())
name String @db.VarChar(255)
name String @db.VarChar(255) @unique
descriptionTranslationId String @unique

descriptionTranslation Translation @relation(fields: [descriptionTranslationId], references: [id], onDelete: Cascade)
Expand Down Expand Up @@ -490,28 +532,6 @@ model UeWorkTime {
ue Ue @relation(fields: [ueId], references: [id], onDelete: Cascade)
}

model UserPermission {
id String @id
nameTranslationId String @unique
descriptionTranslationId String? @unique

name Translation @relation(name: "userPermissionName", fields: [nameTranslationId], references: [id], onDelete: Cascade)
description Translation? @relation(name: "userPermissionDescription", fields: [descriptionTranslationId], references: [id], onDelete: SetNull)
users UserPermissionAssignement[]
}

model UserPermissionAssignement {
id String @id @default(uuid())
userPermissionId String
userId String
assignedAt DateTime @default(now())
assignedById String?

userPermission UserPermission @relation(fields: [userPermissionId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
assignedBy User? @relation(fields: [assignedById], references: [id], name: "permissions_assigned", onDelete: SetNull)
}

model User {
id String @id @default(uuid())
login String @unique @db.VarChar(50)
Expand All @@ -525,21 +545,21 @@ model User {
infosId String @unique
mailsPhonesId String @unique
socialNetworkId String @unique
privacyId String @unique
privacyId String @unique

userType UserType
timestamps UserTimestamps?
socialNetwork UserSocialNetwork @relation(fields: [socialNetworkId], references: [id])
socialNetwork UserSocialNetwork @relation(fields: [socialNetworkId], references: [id])
bans UserBan[]
rgpd UserRGPD @relation(fields: [rgpdId], references: [id])
rgpd UserRGPD @relation(fields: [rgpdId], references: [id])
bdeContributions UserBDEContribution[]
assoMembership AssoMembership[]
branchSubscriptions UserBranchSubscription[]
formation UserFormation?
preference UserPreference @relation(fields: [preferenceId], references: [id])
infos UserInfos @relation(fields: [infosId], references: [id])
preference UserPreference @relation(fields: [preferenceId], references: [id])
infos UserInfos @relation(fields: [infosId], references: [id])
addresses UserAddress[]
mailsPhones UserMailsPhones @relation(fields: [mailsPhonesId], references: [id])
mailsPhones UserMailsPhones @relation(fields: [mailsPhonesId], references: [id])
otherAttributes UserOtherAttributValue[]
UesSubscriptions UserUeSubscription[]
UeStarVotes UeStarVote[]
Expand All @@ -553,16 +573,15 @@ model User {
repliesReported UeCommentReplyReport[]
gitHubIssues GitHubIssue[]
etuUTTTeam UserEtuUTTTeam[]
// Permissions assigned to the user
permissions UserPermissionAssignement[]
// Permission assigned by the user to other users. Used for access control history
permissionsAssigned UserPermissionAssignement[] @relation(name: "permissions_assigned")
courseExchanges UeCourseExchange[]
courseExchangeReplies UeCourseExchangeReply[]
commentUpvotes UeCommentUpvote[]
timetableGroups UserTimetableGroup[]
homepageWidgets UserHomepageWidget[]
privacy UserPrivacy @relation(fields: [privacyId], references: [id])
privacy UserPrivacy @relation(fields: [privacyId], references: [id])
apiApplications ApiApplication[]
apiKeys ApiKey[]
apiGrants ApiGrantedPermission[]
}

model UserAddress {
Expand Down Expand Up @@ -599,12 +618,12 @@ model UserBDEContribution {
}

model UserBranchSubscription {
id String @id @default(uuid())
userId String
semesterNumber Int @db.SmallInt
createdAt DateTime @default(now())
branchOptionId String
semesterCode String
id String @id @default(uuid())
userId String
semesterNumber Int @db.SmallInt
createdAt DateTime @default(now())
branchOptionId String
semesterCode String

user User @relation(fields: [userId], references: [id])
branchOption UTTBranchOption @relation(fields: [branchOptionId], references: [id])
Expand Down Expand Up @@ -701,11 +720,11 @@ model UserHomepageWidget {
}

model UserPreference {
id String @id @default(uuid())
language Language @default(fr)
wantDaymail Boolean @default(false)
wantDayNotif Boolean @default(false)
wantDiscordUtt Boolean @default(false)
id String @id @default(uuid())
language Language @default(fr)
wantDaymail Boolean @default(false)
wantDayNotif Boolean @default(false)
wantDiscordUtt Boolean @default(false)

user User?
}
Expand All @@ -719,14 +738,14 @@ model UserRGPD {
}

model UserSocialNetwork {
id String @id @default(uuid())
facebook String? @db.VarChar(255)
twitter String? @db.VarChar(255)
instagram String? @db.VarChar(255)
linkedin String? @db.VarChar(255)
twitch String? @db.VarChar(255)
spotify String? @db.VarChar(255)
discord String? @db.VarChar(255)
id String @id @default(uuid())
facebook String? @db.VarChar(255)
twitter String? @db.VarChar(255)
instagram String? @db.VarChar(255)
linkedin String? @db.VarChar(255)
twitch String? @db.VarChar(255)
spotify String? @db.VarChar(255)
discord String? @db.VarChar(255)

user User?
}
Expand Down Expand Up @@ -868,3 +887,13 @@ enum AddressPrivacy {
ADDRESS_PRIVATE
ALL_PUBLIC
}

enum Permission {
API_SEE_OPINIONS_UE
API_UPLOAD_ANNAL
API_MODERATE_ANNAL
API_MODERATE_COMMENTS

USER_SEE_DETAILS
USER_UPDATE_DETAILS
}
8 changes: 7 additions & 1 deletion prisma/seed/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ function fakeSafeUniqueData<T extends keyof FakeEntityMap, K extends keyof Entit
* Extends the faker module with custom functions.
* These functions are used to generate values for the database.
* This is the schema of the extension:
* {@code {db: { [tableName]: { [columnName]: () => value } } } }
* {@code { db: { [tableName]: { [columnName]: () => value } } } }
*/
declare module '@faker-js/faker' {
export interface Faker {
Expand All @@ -120,6 +120,9 @@ declare module '@faker-js/faker' {
assoMembershipRole: {
position: () => number;
};
ueStarCriterion: {
name: () => string;
}
};
}
}
Expand Down Expand Up @@ -178,6 +181,9 @@ Faker.prototype.db = {
() => Math.max(...(registeredUniqueValues.assoMembershipRole?.position ?? [0])) + 1,
),
},
ueStarCriterion: {
name: () => fakeSafeUniqueData('ueStarCriterion', 'name', faker.word.adjective),
},
};

export function generateTranslation(rng: () => string = faker.random.words) {
Expand Down
Loading
Loading