From d291a0e0b79c099e3864df17f24517c4cc119cdb Mon Sep 17 00:00:00 2001 From: anatole <anatole.romon@polytechnique.edu> Date: Mon, 5 Mar 2018 12:48:24 +0100 Subject: [PATCH] docu + travail sur les meta-groupes --- .../20180305111321_metaGroup_member_table.js | 13 +++ db/seeds/01_create_groups.js | 8 ++ db/seeds/05_metagroup_membership.js | 23 +++++ src/graphql/connectors/connectors.js | 96 ++++++++++++++++++- src/graphql/resolvers.js | 27 ++++-- src/graphql/typeDefs.js | 3 +- 6 files changed, 161 insertions(+), 9 deletions(-) create mode 100644 db/migrations/20180305111321_metaGroup_member_table.js create mode 100644 db/seeds/05_metagroup_membership.js diff --git a/db/migrations/20180305111321_metaGroup_member_table.js b/db/migrations/20180305111321_metaGroup_member_table.js new file mode 100644 index 0000000..ab7268d --- /dev/null +++ b/db/migrations/20180305111321_metaGroup_member_table.js @@ -0,0 +1,13 @@ + +exports.up = function(knex, Promise) { + return knex.schema.createTable('meta_group_membership', function (table){ + table.timestamp(true, true); + table.string('member_uid').notNullable(); + table.string('union_uid').notNullable(); + table.enum('status', ['admin', 'speaker', 'basic']).notNullable(); + }); +}; + +exports.down = function(knex, Promise) { + return knex.schema.dropTable('meta_group_membership'); +}; diff --git a/db/seeds/01_create_groups.js b/db/seeds/01_create_groups.js index ee9c1ce..fb2d494 100644 --- a/db/seeds/01_create_groups.js +++ b/db/seeds/01_create_groups.js @@ -21,6 +21,14 @@ exports.seed = function(knex, Promise) { school: 'polytechnique', parentuid: 'kes', type : 'simple' + },{ + name: 'Bôbar', + uid: 'bob', + description : "Viens. On est bien", + website: 'bôbar.binet.fr', + school: 'polytechnique', + parentuid: 'kes', + type : 'simple' },{ name: 'Kès', uid: 'kes', diff --git a/db/seeds/05_metagroup_membership.js b/db/seeds/05_metagroup_membership.js new file mode 100644 index 0000000..cd15875 --- /dev/null +++ b/db/seeds/05_metagroup_membership.js @@ -0,0 +1,23 @@ + +exports.seed = async function(knex, Promise) { + // Deletes ALL existing entries + await knex('meta_groups').del(); + await knex('meta_group_membership').insert([ + { + member_uid : "br", + union_uid : "federez", + status : "admin" + }, + { + member_uid : "data", + union_uid : "federez", + status : "admin" + }, + { + member_uid : "bob", + union_uid : "bsckbl", + status : "admin" + } + ]); + return; +}; diff --git a/src/graphql/connectors/connectors.js b/src/graphql/connectors/connectors.js index fdf5bcf..6b431f7 100644 --- a/src/graphql/connectors/connectors.js +++ b/src/graphql/connectors/connectors.js @@ -10,6 +10,51 @@ import { exportAllDeclaration } from 'babel-types'; export { renseignerSurUtilisateur, repliquerTOLdesIds, listerMembres }; +/* +Le tag @rights et la gestion des authorisations + +Le système GraphQL est pensé comme l'interface par laquelle les utilisateurs +intéragissent avec sigma, les graphismes en moins. +Pour cette raison, et pour des questions de sécurité, il faut partir du principe que +l'utilisateir peut rédiger n'importe quelle requête, et que c'est au niveau des resolvers +que la sécurité ce déroule. C'est dans cette optique que j'utilise le tag @rights + +Commençons par un rappel sur le fonctionnement des droits. +Chaque utilisateur a un certain niveau de droit sur chaque groupe. Ce niveau de droit indique +ce qu'il a le droit de savoir et de faire. Chaque niveau est inclu dans les niveaus supérieur. +Les différends niveaux sont : +none - aucun droit +viewer : l'utilisateur a visibilité sur le groupe. Il sait que le groupe existe, et a accès à un certain nombre d'infos. +member : l'utilisateur est membre du groupe +speaker : l'utilisateur peut parler au nom du groupe. Il a le droit de publier des annonces et d'organiser des évènements +admin : l'utilisateur a tous les droits sur le groupe + +Certaines fonctions de connectors effectuent des vérifications d'authorisations avant +de renvoyer une réponse, d'autres non. Pour être sur qu'on ne renvoie jamais de réponse +sans avoir au préalable éffectué les bonnes vérifications, chaque fonction possède dans sa +description un attribut droit, qui décrit les droits que fournit cette fonction. + +La valeur de @rights peut être : +super - la fonction n'effectue aucune vérification, et renvoie le resultat demandé +admin( groupUID ) - la fonction ne fait que ce qu'un admin du groupe indiqué aurait le droit de faire +speaker( groupUID ), member( groupUID ), veiwer( groupUID ) - même chose +user - la fonction ne fait que ce que l'utiliateur a le droit de faire (vérifications via l'argument user) + +La procédure a suivre est la suivante : quand une fonction possède un certain niveau de droit, +elle ne peut appeler une fonction possédant un niveau de droit plus large que si +1 ) on a au préalable vérifié que l'utilisateur possédait effectivement ces droits. +ou +2 ) on s'est assuré que l'opération effectuée par cet appel particulier de la fonction était dans les droits +de l'utilisateur + +Les resolvers de base de mutation et query ont des droits user. + +Les fonctions qui ne modifient pas la BDD et ne renvoient pas de données sur la BDD n'ont pas de rights. +*/ + + + + /** * @summary Génère une promise. * @function @@ -37,6 +82,7 @@ const quickPromise = (val) => { * @arg {Object} groupUID - L'id du groupe dont on veut connaître le type. * @return {Promise(String)} Un string représentant le type du groupe. * Peut être "SimpleGroup" ou "MetaGroup". Renvoie `Undefined` si le groupe n'existe pas + * @rights super */ export const getGroupType = (user, groupUID) => { return knex('simple_groups').select('uid').where('uid', groupUID).then( sg_res => { @@ -60,6 +106,7 @@ export const getGroupType = (user, groupUID) => { * @arg {String} uid - L'uid du groupe dont on veut les administrateurs. * @return {Promise} Retour de requête knex. Promise qui renvera une liste * de tous les utilisateurs ayant droit d'admin sur le groupe + * rights member(groupUID) */ export const getUsersWithAdminRights = (user, groupUID) => { return getGroupType(user, groupUID).then( groupType => { @@ -98,6 +145,7 @@ export const getUsersWithAdminRights = (user, groupUID) => { * @arg {Object} user - Objet contenant un attribut `uid` de type `string`. * User représente l'utilisateur qui a effectué la requête. * @return {Promise} Retour de requête knex. Liste de tous les groupes que l'utilisateur a le droit de voire. + * rights user */ export const hasAdminRights = (user, groupUID) => { if(user.uid == "anatole.romon") @@ -114,6 +162,7 @@ export const hasAdminRights = (user, groupUID) => { * @arg {Object} user - Objet contenant un attribut `uid` de type `string`. * User représente l'utilisateur qui a effectué la requête. * @return {Promise(Callback)} callback contruisant une requête knex pour une table de tous les id visibles. + * @rights user */ export const getVisibleGroupCallback = (user) => { return listerGroupes(user, user.uid).then(group_ids => { @@ -166,6 +215,7 @@ function getGroupTableName(wantedType){ * @arg {String} uid - Identifiant du groupe voulu. * @arg {String} type - Type de groupe voulu. `"simple"`, `"meta"` ou `"all"`. * @return {Promise(group)} Retour de requête knex. Le groupe demandé, si l'utilisateur a le droit de la voire. + * @rights user */ export async function getGroupIfVisible(user, groupUID, type="all"){ let group_table_name = getGroupTableName(type); @@ -199,6 +249,7 @@ export async function getAllVisibleMetaGroups (user){ * @arg {Object} user - Représente l'utilisateur qui a effectué la requête. * @arg {String} wantedType - Type de groupe voulu : `"simple"`, `"meta"` ou `"all"`. * @return {Promise} Retour de requête knex. Liste de tous les groupes que l'utilisateur a le droit de voire. + * @rights user */ export async function getAllVisibleGroups(user){ let all_simple_groups = await getAllVisibleSimpleGroups(user); @@ -211,6 +262,7 @@ export async function getAllVisibleGroups(user){ * @arg {Object} user - Représente l'utilisateur qui a effectué la requête. * @arg {Object} groupUID - L'id du groupe dont on veu savoir si l'utilisateur est membre. * @return {Promise(Boolean)} Boolean indiquant si l'utilisateur est membre du groupe. + * @rights user */ export const isMember = (user, groupUID) => { return listerGroupes(user, user.uid).then(group_ids => group_ids && group_ids.indexOf(groupUID) != -1); @@ -221,6 +273,9 @@ export const isMember = (user, groupUID) => { * @desc RASifie le string initialUID si necessaire (ramené à de l'ASCCI sans espace), puis si l'uid est deja pris rajoute un n a la fin et reteste * @arg {String} uid - L'uid du groupe dont on veut les administrateurs. * @return {Promise} Retour de requête knex. Promise qui renvera une liste de tous les utilisateurs ayant droit d'admin sur le groupe + * @rights user + * remarque : n'importe qui peut tester si un groupe existe en demandant a créer un groupe avec ce nom la et en regardant si + * son UID a été modifié. Je ne vois pas comment contourner ce problème, c'est donc une faille permanente de sigma. */ export function getAvailablegroupUID(initialUID){ let rasUID = initialUID.replace(' ', '_').replace(/\W/g, '').toLowerCase(); @@ -243,6 +298,7 @@ export function getAvailablegroupUID(initialUID){ * @arg {Object} user - L'utilisateur qui effectue la requête. * @arg {Object} args - Les arguments envoyés à la mutation. Cf le schéma GraphQL * @return {Promise} Retour de requête knex. Le groupe qui vient d'être créé. En cas d'echec, renvoie une erreur. + * @rights admin (args.parentuid) */ export async function createSubgroup(user, args){ if (typeof args.parentuid != 'string') @@ -276,6 +332,7 @@ export async function createSubgroup(user, args){ * @arg {Object} user - L'utilisateur qui effectue la requête. * @arg {Object} args - Les arguments envoyés à la mutation. Cf le schéma GraphQL * @return {Promise} Retour de requête knex. Le groupe qui vient d'être créé. En cas d'echec, renvoie une erreur. + * @rights user */ export async function createGroupIfLegal(user, args){ if( await hasAdminRights(user, args.parentuid) ){ @@ -292,6 +349,7 @@ export async function createGroupIfLegal(user, args){ * @arg {Object} user - L'utilisateur qui effectue la requête. * @arg {String} args - L'identifiant du groupe qui reçoit la requête. * @return {Promise(Object)} Retour de requête knex. Toutes les requêtes destinées au groupe. + * @rights admin(recipientUID) */ export function getUserJoinGroupRequests(user, recipientUID){ return knex.select('id', 'useruid', 'message').from('user_join_group') @@ -307,6 +365,7 @@ export function getUserJoinGroupRequests(user, recipientUID){ * @arg {Object} user - L'utilisateur qui effectue la requête. * @arg {String} args - L'identifiant du groupe qui reçoit la requête. * @return {Promise(Object)} Retour de requête knex. Toutes les requêtes destinées au groupe. + * @rights speaker(recipientUID) */ export function getGroupJoinEventRequests(user, recipientUID){ return knex.select('id', 'senderuid', 'eventuid', 'message').from('group_join_event') @@ -323,6 +382,7 @@ export function getGroupJoinEventRequests(user, recipientUID){ * @arg {Object} user - L'utilisateur qui effectue la requête. * @arg {String} args - L'identifiant du groupe qui reçoit la requête. * @return {Promise(Object)} Retour de requête knex. Toutes les requêtes destinées au groupe. + * @rights speaker(recipientUID) */ export const getYourGroupHostEventRequests = (user, recipientUID) => { return knex.select('id', 'senderuid', 'eventuid', 'message').from('your_group_host_event') @@ -362,10 +422,11 @@ export const getEvent = (user, eventID) => { }; /** - * @function Renvoie simplement un groupe en fonction de son identifiant. + * @summary Renvoie simplement un groupe en fonction de son identifiant. * @param {Object} user - Utilisateur effectuant la requête. * @param {String} groupUID - Identifiant unique du groupe. * @author manifold + * @rights super */ export const getGroup = (user, groupUID) => { // Une sélection sur une table renvoie un tableau. @@ -374,14 +435,47 @@ export const getGroup = (user, groupUID) => { return knex.select().from('groups').where('uid',groupUID).then(results => results [0]); }; +/** + * @summary Renvoie simplement un groupe simple en fonction de son identifiant. + * @param {Object} user - Utilisateur effectuant la requête. + * @param {String} groupUID - Identifiant unique du groupe. + * @author manifold + * @rights super + */ export const getSimpleGroup = (user, groupUID) => { return knex.select().from('simple_groups').where('uid',groupUID).then(results => results [0]); }; +/** + * @summary Renvoie simplement un meta groupe en fonction de son identifiant. + * @param {Object} user - Utilisateur effectuant la requête. + * @param {String} groupUID - Identifiant unique du groupe. + * @author manifold + * @rights super + */ export const getMetaGroup = (user, groupUID) => { return knex.select().from('meta_groups').where('uid',groupUID).then(results => results [0]); }; export const getMetaGroupAdminMembers = (user, metaGroupUID) => { return quickPromise([]); +}; + +/** + * @function getMetaGroupMembers + * @summary Renvoie tous les membres d'un meta-groupe. + * @param {Object} user - Utilisateur effectuant la requête. + * @param {String} metaGroupUID - Identifiant unique du groupe. + * @return {Promise(callback)} a callback to build a query for the members of a group + * It doesn't need to be a promise, but I figure having all of my functions return promises is + * easier than keeping track of which functions do and do not return promises. + * @author akka vodol + * @rights member(metaGroupUID) + */ +export async function getMetaGroupMembersCallback(user, metaGroupUID){ + return function(query_builder){ + return query_builder.distinct().select().from('groups') + .innerJoin('meta_group_membership', 'groups.uid', 'meta_group_membership.member_uid') + .where('meta_group_membership.union_uid', '=', metaGroupUID); + }; }; \ No newline at end of file diff --git a/src/graphql/resolvers.js b/src/graphql/resolvers.js index 2c04e50..d7b681c 100644 --- a/src/graphql/resolvers.js +++ b/src/graphql/resolvers.js @@ -16,13 +16,15 @@ import { connect } from 'http2'; export const resolvers = { Query: { - asAdmin: (obj, args, context) => { - return connectors.hasAdminRights(context.user, args.groupUID).then(res => { - if(res) - return {groupUID : args.groupUID}; - else - throw "You do not have admin rights over this group"; - }); + asAdmin: async function (obj, args, context){ + if(await connectors.hasAdminRights(context.user, args.groupUID)) + return {groupUID : args.groupUID}; + else + throw "You do not have admin rights over this group"; + }, + + asMember: function (obj, args, context){ + return {groupUID : args.groupUID}; }, accessGroups: (obj, args, context) => { @@ -104,6 +106,17 @@ export const resolvers = { } }, + MemberQuery: { + isMember: (obj, args, context) => { + return true; + }, + + allMembers: async function (obj, args, context){ + let cb = await connectors.getMetaGroupMembersCallback(context.user, obj.groupUID); + return cb(knex); + } + }, + AllRequests: { userJoinGroup : (obj, args, context) => { return connectors.getUserJoinGroupRequests(context.user, obj.groupUID); diff --git a/src/graphql/typeDefs.js b/src/graphql/typeDefs.js index e9e9069..f226961 100644 --- a/src/graphql/typeDefs.js +++ b/src/graphql/typeDefs.js @@ -7,7 +7,7 @@ const RootTypes = ` asAdmin(groupUID: ID): AdminQuery asSpeaker(groupUID: ID): AdminQuery - asMember(groupUID: ID): AdminQuery + asMember(groupUID: ID): MemberQuery asViewer(groupUID: ID): AdminQuery } @@ -124,6 +124,7 @@ const subQueries = ` type MemberQuery{ isMember: Boolean + allMembers : [Group] } type ViewerQuery{ -- GitLab