Commit 1b17cbd7 authored by Luc Bourdot's avatar Luc Bourdot

Merge branch 'nextcloud_groups' into 'dev'

Nextcloud groups

See merge request !223
parents 5f26dddf 3d55071a
Pipeline #2326 passed with stages
in 4 minutes and 48 seconds
......@@ -95,7 +95,8 @@
"members": "Members",
"animators": "Animators",
"admins": "Admins",
"applications": "Applications ID"
"applications": "Applications ID",
"nextcloud": "Create groupe in Nextcloud"
}
},
"services": {
......@@ -186,6 +187,13 @@
"excludeRole": "Excluded Role",
"keycloakId": "Keycloak Id"
}
},
"nextcloud": {
"groupExists": "Group already exists in Nextcloud",
"addGroupError": "Error, could not create group in Nextcloud",
"addGroupFolderError": "Error, could not create group folder in Nextcloud",
"removeGroupError": "Warning, could not delete group in Nextcloud",
"removeGroupFolderError": "Warning, could not delete group folder in Nextcloud"
}
},
"components": {
......@@ -638,7 +646,8 @@
"content": "Long description of the group",
"save": "Save",
"update": "Update",
"cancel": "Cancel"
"cancel": "Cancel",
"nextcloud": "Create group in Nextcloud"
},
"SingleGroupPage": {
"backToList": "Back to the list",
......
......@@ -95,7 +95,8 @@
"members": "Membres",
"animators": "Animateurs",
"admins": "Administrateurs",
"applications": "ID de l'application"
"applications": "ID de l'application",
"nextcloud": "Créer le groupe dans Nextcloud"
}
},
"services": {
......@@ -186,6 +187,13 @@
"excludeRole": "Rôle Exclus",
"keycloakId": "Identifiant Keycloak"
}
},
"nextcloud": {
"groupExists": "Ce groupe existe déjà dans Nextcloud",
"addGroupError": "Erreur lors de la création du groupe dans Nextcloud",
"addGroupFolderError": "Erreur lors de la création du partage dans Nextcloud",
"removeGroupError": "Attention, le groupe n'a pas pu être supprimé dans Nextcloud",
"removeGroupFolderError": "Attention, le dossier n'a pas pu être supprimé dans Nextcloud"
}
},
"components": {
......@@ -641,7 +649,8 @@
"content": "Longue description du groupe",
"save": "Sauvegarder",
"update": "Mettre à jour",
"cancel": "Annuler"
"cancel": "Annuler",
"nextcloud": "Créer le groupe dans Nextcloud"
},
"SingleGroupPage": {
"backToList": "Retour à la liste",
......
import axios from 'axios';
import { Meteor } from 'meteor/meteor';
import AppRoles from './users/users';
import AppRoles from '../users/users';
class KeyCloakClient {
constructor() {
......@@ -29,7 +29,9 @@ class KeyCloakClient {
const { adminPassword } = Meteor.settings.keycloak;
return axios.post(
`${this.kcURL}/realms/master/protocol/openid-connect/token`,
`username=${adminUser}&password=${adminPassword}&grant_type=password&client_id=admin-cli`,
`username=${encodeURIComponent(adminUser)}&password=${encodeURIComponent(
adminPassword,
)}&grant_type=password&client_id=admin-cli`,
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
......@@ -188,6 +190,22 @@ class KeyCloakClient {
);
}
_addRoleToGroup(groupId, groupName, token) {
return this._getRoleId(groupName, token).then((roleId) =>
axios.post(
`${this.kcURL}/admin/realms/${this.kcRealm}/groups/${groupId}/role-mappings/clients/${this.clientId}`,
[{ name: groupName, id: roleId }],
{
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
Authorization: `Bearer ${token}`,
},
},
),
);
}
_addGroup(groupName, token) {
return this._addRole(groupName, token).then(() => {
return axios
......@@ -207,19 +225,7 @@ class KeyCloakClient {
.then(() => this._getGroupId(groupName, token))
.then((groupId) => {
console.log(`Keycloak: group ${groupName} added (id ${groupId})`);
return this._getRoleId(groupName, token).then((roleId) => {
return axios.post(
`${this.kcURL}/admin/realms/${this.kcRealm}/groups/${groupId}/role-mappings/clients/${this.clientId}`,
[{ name: groupName, id: roleId }],
{
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
Authorization: `Bearer ${token}`,
},
},
);
});
return this._addRoleToGroup(groupId, groupName, token);
});
});
}
......@@ -254,6 +260,59 @@ class KeyCloakClient {
});
}
_updateGroup(oldName, groupName) {
this._getToken()
.then((token) => {
// search group id
return this._getGroupId(oldName, token).then((groupId) => {
if (groupId === undefined) {
console.log(`Keycloak: could not find group ${oldName}`);
return null;
}
// delete associated role
return this._removeRole(oldName, token).then(() => {
// update group
return axios
.put(
`${this.kcURL}/admin/realms/${this.kcRealm}/groups/${groupId}`,
{ name: groupName },
{
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
Authorization: `Bearer ${token}`,
},
},
)
.then(() => this._addRole(groupName, token))
.then(() =>
this._addRoleToGroup(groupId, groupName, token).then(() =>
console.log(`Keycloak: changed group name from ${oldName} to ${groupName}`),
),
);
});
});
})
.catch((error) =>
console.log(
`Keycloak: Error updating group ${oldName}`,
error.response && error.response.data ? error.response.data : error,
),
);
}
updateGroupWithRoles(oldName, groupName) {
AppRoles.filter((role) => role !== 'candidate').forEach((role) => {
const oldRole = `${role}_${oldName}`;
const newRole = `${role}_${groupName}`;
this._updateGroup(oldRole, newRole);
});
}
updateGroup(oldName, groupName) {
this._updateGroup(oldName, groupName);
}
_removeGroup(groupName) {
this._getToken()
.then((token) => {
......
import axios from 'axios';
import { Meteor } from 'meteor/meteor';
const _checkFolderActive = function (response) {
// checks that 'Group Folder' API is responding
if (response.data === undefined || response.data.ocs === undefined) {
console.log(`Nexcloud: ERROR, make sure 'Group Folders' application is active`);
return false;
}
return true;
};
class NextcloudClient {
constructor() {
const ncURL = Meteor.settings.public.nextcloudURL || '';
const ncUser = (Meteor.settings.nextcloud && Meteor.settings.nextcloud.nextcloudUser) || '';
const ncPassword = (Meteor.settings.nextcloud && Meteor.settings.nextcloud.nextcloudPassword) || '';
this.nextURL = `${ncURL}/ocs/v1.php/cloud`;
this.appsURL = `${ncURL}/apps`;
this.basicAuth = Buffer.from(`${ncUser}:${ncPassword}`, 'binary').toString('base64');
}
groupExists(groupName) {
return axios
.get(`${this.nextURL}/groups`, {
params: {
search: groupName,
},
headers: {
Accept: 'application/json',
Authorization: `Basic ${this.basicAuth}`,
'OCS-APIRequest': true,
},
})
.then((response) => {
return response.data.ocs.data.groups.includes(groupName);
})
.catch((error) => {
console.log(`Nextcloud: ERROR getting group ${groupName}`);
console.log(error.response && error.response.data ? error.response.data : error);
return false;
});
}
addGroup(groupName) {
return axios
.post(
`${this.nextURL}/groups`,
{
groupid: groupName,
},
{
headers: {
'Content-Type': 'application/json',
Authorization: `Basic ${this.basicAuth}`,
'OCS-APIRequest': true,
},
},
)
.then((response) => {
const infos = response.data.ocs.meta;
if (infos.status === 'ok') {
console.log(`Nextcloud: group ${groupName} added`);
} else {
console.log(`Nextcloud: ERROR adding group ${groupName} (${infos.statuscode} - ${infos.message})`);
}
return infos.status === 'ok' ? infos.status : infos.message;
})
.catch((error) => {
console.log(`Nextcloud: ERROR adding group ${groupName}`);
console.log(error.response && error.response.data ? error.response.data : error);
return `Nextcloud: ERROR adding group ${groupName}`;
});
}
_addGroupToFolder(groupName, folderName, folderId) {
return axios
.post(
`${this.appsURL}/groupfolders/folders/${folderId}/groups`,
{
group: groupName,
},
{
params: { format: 'json' },
headers: {
'Content-Type': 'application/json',
Authorization: `Basic ${this.basicAuth}`,
'OCS-APIRequest': true,
},
},
)
.then((response) => {
if (response.data.ocs.meta.status === 'ok') {
return axios
.post(
`${this.appsURL}/groupfolders/folders/${folderId}/groups/${groupName}`,
{
// set permissions to : create, read, update, delete (not share)
permissions: 15,
},
{
params: { format: 'json' },
headers: {
'Content-Type': 'application/json',
Authorization: `Basic ${this.basicAuth}`,
'OCS-APIRequest': true,
},
},
)
.then((resp) => resp.data.ocs.meta.status === 'ok');
}
console.log(`Nextcloud: could not assign group ${groupName} to folder ${folderName}`);
return false;
});
}
_addQuotaToFolder(folderId) {
// get quota (in bytes) from settings, or -3 if not set (unlimited)
const quota = Meteor.settings.nextcloud.nextcloudQuota || -3;
return axios
.post(
`${this.appsURL}/groupfolders/folders/${folderId}/quota`,
{
quota,
},
{
params: { format: 'json' },
headers: {
'Content-Type': 'application/json',
Authorization: `Basic ${this.basicAuth}`,
'OCS-APIRequest': true,
},
},
)
.then((response) => {
const infos = response.data.ocs.meta;
if (_checkFolderActive(response) && infos.status === 'ok') {
return true;
}
return false;
});
}
addGroupFolder(groupName, folderName) {
// creates a new group folder and configure access for group users
return axios
.post(
`${this.appsURL}/groupfolders/folders`,
{
mountpoint: folderName,
},
{
params: { format: 'json' },
headers: {
'Content-Type': 'application/json',
Authorization: `Basic ${this.basicAuth}`,
'OCS-APIRequest': true,
},
},
)
.then((response) => {
const infos = response.data.ocs.meta;
if (_checkFolderActive(response) && infos.status === 'ok') {
console.log(`Nextcloud: group folder ${folderName} added`);
return this._addGroupToFolder(groupName, folderName, response.data.ocs.data.id).then((resp) => {
if (resp === true) {
console.log(`Nextcloud: access and permissions set for group folder ${folderName}`);
return this._addQuotaToFolder(response.data.ocs.data.id).then((respQuota) => {
if (respQuota) {
console.log(`Nextcloud: quota set for group folder ${folderName}`);
} else {
console.log(`Nextcloud: ERROR setting quota on group folder ${folderName}`);
}
return respQuota;
});
}
console.log(`Nextcloud: ERROR settings group permissions for group folder ${folderName}`);
return resp;
});
}
console.log(`Nextcloud: ERROR adding group folder ${folderName}`);
return false;
})
.catch((error) => {
console.log(`Nextcloud: ERROR adding group folder ${folderName}`);
console.log(error.response && error.response.data ? error.response.data : error);
return false;
});
}
removeGroupFolder(groupName) {
return axios
.get(`${this.appsURL}/groupfolders/folders`, {
params: { format: 'json' },
headers: {
Accept: 'application/json',
Authorization: `Basic ${this.basicAuth}`,
'OCS-APIRequest': true,
},
})
.then((response) => {
if (_checkFolderActive(response) && response.data.ocs.meta.status === 'ok') {
// find groupFolder ID for groupName
const folders = Object.values(response.data.ocs.data).filter((entry) => {
return entry.mount_point === groupName && Object.keys(entry.groups).includes(groupName);
});
return Promise.all(
folders.map((folder) => {
// check that folder is linked to group
return axios
.delete(`${this.appsURL}/groupfolders/folders/${folder.id}`, {
params: { format: 'json' },
headers: {
Accept: 'application/json',
Authorization: `Basic ${this.basicAuth}`,
'OCS-APIRequest': true,
},
})
.then((resp) => {
const infos = resp.data.ocs.meta;
if (infos.status === 'ok') {
console.log(`Nextcloud: removed group folder ${folder.id} (${folder.mount_point})`);
return true;
}
console.log(`Nextcloud: ERROR deleting group folder ${folder.id} (${infos.message})`);
return false;
});
}),
).then((responses) => !responses.includes(false));
}
return false;
});
}
removeGroup(groupName) {
return axios
.delete(`${this.nextURL}/groups/${groupName}`, {
headers: {
Authorization: `Basic ${this.basicAuth}`,
'OCS-APIRequest': true,
},
})
.then((response) => {
const infos = response.data.ocs.meta;
if (infos.status === 'ok') {
console.log(`Nextcloud: group ${groupName} removed`);
} else {
console.log(`Nextcloud: Error removing group ${groupName} (${infos.message})`);
}
return infos.status === 'ok';
})
.catch((error) => {
console.log(`Nextcloud: ERROR removing group ${groupName}`);
console.log(error.response && error.response.data ? error.response.data : error);
return false;
});
}
}
const nextEnabled = Meteor.settings.public.enableNextcloud === true;
const nextClient = Meteor.isServer && nextEnabled ? new NextcloudClient() : null;
export default nextClient;
......@@ -121,6 +121,11 @@ Groups.schema = new SimpleSchema(
defaultValue: 0,
label: getLabel('api.groups.labels.numCandidates'),
},
nextcloud: {
type: Boolean,
defaultValue: false,
label: getLabel('api.groups.labels.nextcloud'),
},
},
{ tracker: Tracker },
);
......@@ -149,6 +154,7 @@ Groups.publicFields = {
type: 1,
owner: 1,
numCandidates: 1,
nextcloud: 1,
};
Groups.allPublicFields = {
content: 1,
......
......@@ -9,7 +9,8 @@ import i18n from 'meteor/universe:i18n';
import { isActive, getLabel } from '../utils';
import Groups from './groups';
import { addGroup, removeElement } from '../personalspaces/methods';
import kcClient from '../kcClient';
import kcClient from '../appclients/kcClient';
import nextClient from '../appclients/nextcloud';
export const favGroup = new ValidatedMethod({
name: 'groups.favGroup',
......@@ -22,8 +23,8 @@ export const favGroup = new ValidatedMethod({
throw new Meteor.Error('api.groups.favGroup.notPermitted', i18n.__('api.users.mustBeLoggedIn'));
}
// check group existence
const service = Groups.findOne(groupId);
if (service === undefined) {
const group = Groups.findOne(groupId);
if (group === undefined) {
throw new Meteor.Error('api.groups.favGroup.unknownService', i18n.__('api.groups.unknownGroup'));
}
const user = Meteor.users.findOne(this.userId);
......@@ -64,6 +65,29 @@ export const unfavGroup = new ValidatedMethod({
},
});
function _createGroup({ name, type, content, description, nextcloud, userId }) {
try {
const groupId = Groups.insert({
name,
type,
content,
description,
owner: userId,
admins: [userId],
active: true,
nextcloud,
});
Roles.addUsersToRoles(userId, 'admin', groupId);
favGroup._execute({ userId }, { groupId });
} catch (error) {
if (error.code === 11000) {
throw new Meteor.Error('api.groups.createGroup.duplicateName', i18n.__('api.groups.groupAlreadyExist'));
} else {
throw error;
}
}
}
export const createGroup = new ValidatedMethod({
name: 'groups.createGroup',
validate: new SimpleSchema({
......@@ -71,35 +95,36 @@ export const createGroup = new ValidatedMethod({
type: { type: SimpleSchema.Integer, min: 0, label: getLabel('api.groups.labels.type') },
description: { type: String, label: getLabel('api.groups.labels.description') },
content: { type: String, label: getLabel('api.groups.labels.content') },
}).validator(),
nextcloud: { type: Boolean, defaultValue: false, label: getLabel('api.groups.labels.nextcloud') },
}).validator({ clean: true }),
run({ name, type, content, description }) {
run({ name, type, content, description, nextcloud }) {
if (!isActive(this.userId)) {
throw new Meteor.Error('api.groups.createGroup.notLoggedIn', i18n.__('api.users.mustBeLoggedIn'));
}
try {
const groupId = Groups.insert({
name,
type,
content,
description,
owner: this.userId,
admins: [this.userId],
active: true,
});
Roles.addUsersToRoles(this.userId, 'admin', groupId);
favGroup._execute({ userId: this.userId }, { groupId });
} catch (error) {
if (error.code === 11000) {
throw new Meteor.Error('api.groups.createGroup.duplicateName', i18n.__('api.groups.groupAlreadyExist'));
} else {
throw error;
}
}
if (kcClient) {
// create associated groups and roles in keycloak
kcClient.addGroup({ name });
}
if (nextcloud && nextClient) {
// create associated group in Nextcloud
return nextClient.addGroup(name).then((response) => {
if (response === 'ok') {
return nextClient.addGroupFolder(name, name).then((res) => {
if (res === false)
throw new Meteor.Error(
'api.groups.createGroup.nextcloudError',
i18n.__('api.nextcloud.addGroupFolderError'),
);
else return _createGroup({ name, type, content, description, nextcloud, userId: this.userId });
});
}
const msg =
response === 'group exists' ? i18n.__('api.nextcloud.groupExists') : i18n.__('api.nextcloud.addGroupError');
throw new Meteor.Error('api.groups.createGroup.nextcloudError', msg);
});
}
return _createGroup({ name, type, content, description, nextcloud, userId: this.userId });
},
});
......@@ -131,9 +156,44 @@ export const removeGroup = new ValidatedMethod({
Groups.remove(groupId);
// remove from users favorite groups
Meteor.users.update({ favGroups: { $all: [groupId] } }, { $pull: { favGroups: groupId } }, { multi: true });
if (nextClient && group.nextcloud) {
// remove group from nextcloud if it exists
return nextClient.groupExists(group.name).then((resExists) => {
if (resExists) {
return nextClient.removeGroupFolder(group.name).then((response) => {
if (response)
return nextClient.removeGroup(group.name).then((res) => {
if (res === false)
throw new Meteor.Error(
'api.groups.removeGroup.nextcloudError',
i18n.__('api.nextcloud.removeGroupError'),
);
});
throw new Meteor.Error(
'api.groups.removeGroup.nextcloudError',
i18n.__('api.nextcloud.removeGroupFolderError'),
);
});
}
return null;
});
}
return null;
},
});
function _updateGroup(groupId, groupData) {
try {
Groups.update({ _id: groupId }, { $set: groupData });
} catch (error) {
if (error.code === 11000) {
throw new Meteor.Error('api.groups.updateGroup.duplicateName', i18n.__('api.groups.groupAlreadyExist'));
} else {
throw error;
}
}
}
export const updateGroup = new ValidatedMethod({
name: 'groups.updateGroup',
validate: new SimpleSchema({
......@@ -156,6 +216,7 @@ export const updateGroup = new ValidatedMethod({
'data.active': { type: Boolean, optional: true, label: getLabel('api.groups.labels.active') },
'data.groupPadId': { type: String, optional: true, label: getLabel('api.groups.labels.groupPadId') },
'data.digest': { type: String, optional: true, label: getLabel('api.groups.labels.digest') },
'data.nextcloud': { type: Boolean, optional: true, label: getLabel('api.groups.labels.nextcloud') },
}).validator({ clean: true }),
run({ groupId, data }) {
......@@ -178,15 +239,38 @@ export const updateGroup = new ValidatedMethod({
} else {
groupData = { ...data };
}
try {
Groups.update({ _id: groupId }, { $set: groupData });
} catch (error) {
if (error.code === 11000) {
throw new Meteor.Error('api.groups.updateGroup.duplicateName', i18n.__('api.groups.groupAlreadyExist'));
} else {
throw error;
}
// update group in keycloak if name has changed
if (kcClient && groupData.name && groupData.name !== group.name) {
kcClient.updateGroup(group.name, groupData.name);
}
// create nextcloud group if needed
const nextRequired = data.nextcloud === true || (data.nextcloud === undefined && group.nextcloud === true);
if (nextClient && nextRequired) {
const groupName = groupData.name || group.name;
return nextClient.groupExists(groupName).then((resExists) => {
if (resExists === false) {
return nextClient.addGroup(groupName).then((response) => {
if (response === 'ok') {
return nextClient.addGroupFolder(groupName, groupName).then((res) => {
if (res === false)
throw new Meteor.Error(
'api.groups.updateGroup.nextcloudError',
i18n.__('api.nextcloud.addGroupFolderError'),
);
_updateGroup(groupId, groupData);
});
}
const msg =
response === 'group exists'
? i18n.__('api.nextcloud.groupExists')
: i18n.__('api.nextcloud.addGroupError');
throw new Meteor.Error('api.groups.updateGroup.nextcloudError', msg);
});
}
return _updateGroup(groupId, groupData);
});
}
return _updateGroup(groupId, groupData);
},
});
......
......@@ -64,3 +64,14 @@ Migrations.add({
Articles.rawCollection().update({}, { $unset: { visits: true } }, { multi: true });
},
});
Migrations.add({
version: 5,
name: 'Add nextcloud setting to groups',
up: () => {
Groups.update({}, { $set: { nextcloud: false } }, { multi: true });
},
down: () => {
Groups.rawCollection().update({}, { $unset: { nextcloud: true } }, { multi: true });
},
});
......@@ -15,7 +15,7 @@ import { structures } from '../structures';
import { favGroup, unfavGroup } from '../../groups/methods';
import PersonalSpaces from '../../personalspaces/personalspaces';
import { createRoleNotification, createRequestNotification } from '../../notifications/server/notifsutils';
import kcClient from '../../kcClient';