Java tutorial
/* Copyright "Open Digital Education", 2014 * * This program is published by "Open Digital Education". * You must indicate the name of the software and the company in any production /contribution * using the software and indicate on the home page of the software industry in question, * "powered by Open Digital Education" with a reference to the website: https://opendigitaleducation.com/. * * This program is free software, licensed under the terms of the GNU Affero General Public License * as published by the Free Software Foundation, version 3 of the License. * * You can redistribute this application and/or modify it since you respect the terms of the GNU Affero General Public License. * If you modify the source code and then use this modified source code in your creation, you must make available the source code of your modifications. * * You should have received a copy of the GNU Affero General Public License along with the software. * If not, please see : <http://www.gnu.org/licenses/>. Full compliance requires reading the terms of this license and following its directives. * */ package org.entcore.registry.services.impl; import fr.wseduc.webutils.Either; import fr.wseduc.webutils.collections.Joiner; import org.entcore.common.neo4j.Neo4j; import org.entcore.common.neo4j.Neo4jResult; import org.entcore.common.neo4j.StatementsBuilder; import org.entcore.common.utils.StringUtils; import org.entcore.registry.services.AppRegistryService; import io.vertx.core.Handler; import io.vertx.core.eventbus.Message; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLDecoder; import java.util.ArrayList; import java.util.List; import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; import static fr.wseduc.webutils.Utils.defaultValidationParamsNull; import static org.entcore.common.neo4j.Neo4jResult.*; import static org.entcore.common.neo4j.Neo4jUtils.nodeSetPropertiesFromJson; public class DefaultAppRegistryService implements AppRegistryService { private final Neo4j neo = Neo4j.getInstance(); private static final Logger log = LoggerFactory.getLogger(DefaultAppRegistryService.class); private static Pattern URL_PATTERN = Pattern.compile("^https?://[^\\s/$.?#].[^\\s]*$"); @Override public void listApplications(String structureId, Handler<Either<String, JsonArray>> handler) { String filter = ""; JsonObject params = null; if (structureId != null && !structureId.trim().isEmpty()) { filter = "WHERE NOT(HAS(n.structureId)) OR n.structureId = {structure} "; params = new JsonObject().put("structure", structureId); } String query = "MATCH (n:Application) " + filter + "RETURN n.id as id, n.name as name, n.icon as icon, 'External' IN labels(n) as isExternal"; neo.execute(query, params, validResultHandler(handler)); } @Override public void listRoles(String structureId, Handler<Either<String, JsonArray>> handler) { String filter = ""; JsonObject params = null; if (structureId != null && !structureId.trim().isEmpty()) { filter = "WHERE NOT(HAS(n.structureId)) OR n.structureId = {structure} "; params = new JsonObject().put("structure", structureId); } String query = "MATCH (n:Role) " + filter + "RETURN n.id as id, n.name as name ORDER BY name ASC "; neo.execute(query, params, validResultHandler(handler)); } @Override public void listRolesWithActions(String structureId, Handler<Either<String, JsonArray>> handler) { String filter = ""; JsonObject params = null; if (structureId != null && !structureId.trim().isEmpty()) { filter = "WHERE NOT(HAS(n.structureId)) OR n.structureId = {structure} "; params = new JsonObject().put("structure", structureId); } String query = "MATCH (n:Role) " + filter + "OPTIONAL MATCH n-[r:AUTHORIZE]->a " + "RETURN n.id as id, n.name as name, COLLECT([a.name, a.displayName, a.type]) as actions"; neo.execute(query, params, validResultHandler(handler)); } @Override public void listActions(String application, Handler<Either<String, JsonArray>> handler) { String query = "MATCH (n:Application)-[:PROVIDE]->a " + "WHERE n.name = {name} " + "RETURN a.name as name, a.displayName as displayName, a.type as type"; neo.execute(query, new JsonObject().put("name", application), validResultHandler(handler)); } @Override public void listGroupsWithRoles(String structureId, boolean classGroups, Handler<Either<String, JsonArray>> handler) { String query; JsonObject params = new JsonObject(); if (structureId != null && !structureId.trim().isEmpty()) { String filter = classGroups ? "<-[:BELONGS*0..1]-()" : ""; params.put("structureId", structureId); query = "MATCH (m:Structure)" + filter + "<-[:DEPENDS]-(n:Group) " + "WHERE m.id = {structureId} " + "OPTIONAL MATCH n-[r:AUTHORIZED]->a "; } else { query = "MATCH (n:Group) " + "OPTIONAL MATCH n-[r:AUTHORIZED]->a "; } query += "RETURN distinct n.id as id, n.name as name, n.groupDisplayName as groupDisplayName, " + "COLLECT(a.id) as roles " + "ORDER BY name ASC "; neo.execute(query, params, validResultHandler(handler)); } @Override public void listApplicationsWithActions(String structureId, String actionType, Handler<Either<String, JsonArray>> handler) { String filter = ""; JsonObject params = new JsonObject(); if (structureId != null && !structureId.trim().isEmpty()) { filter = "WHERE NOT(HAS(n.structureId)) OR n.structureId = {structure} "; params.put("structure", structureId); } String query = "MATCH (n:Application) " + filter + "OPTIONAL MATCH n-[r:PROVIDE]->(a:Action) "; if (actionType != null && ("WORKFLOW".equals(actionType) || "RESOURCE".equals(actionType))) { query += "WHERE r IS NULL OR a.type = {actionType} "; params.put("actionType", "SECURED_ACTION_" + actionType); } query += "RETURN n.id as id, n.name as name, COLLECT([a.name, a.displayName, a.type]) as actions"; neo.execute(query, params, validResultHandler(handler)); } @Override public void listApplicationRolesWithGroups(String structureId, String appId, Handler<Either<String, JsonArray>> handler) { String query = "MATCH (a:Application {id: {appId}})-[:PROVIDE]->(:Action)<-[:AUTHORIZE]-(r:Role) " + "WITH r,a " + "MATCH (r)-[:AUTHORIZE]->(:Action)<-[:PROVIDE]-(apps:Application) " + " OPTIONAL MATCH (s:Structure {id: {structureId}})<-[:DEPENDS*1..2]-(g:Group)-[:AUTHORIZED]->(r) " + " WITH r, a, apps, CASE WHEN g IS NOT NULL THEN COLLECT(DISTINCT{ id: g.id, name: g.name }) ELSE [] END as groups " + "RETURN r.id as id, r.name as name, a.id as appId, groups, COUNT(DISTINCT apps) > 1 as transverse"; JsonObject params = new JsonObject().put("appId", appId).put("structureId", structureId); neo.execute(query, params, Neo4jResult.validResultHandler(handler)); } @Override public void createRole(String structureId, JsonObject role, JsonArray actions, Handler<Either<String, JsonObject>> handler) { String query = "MATCH (n:Role) " + "WHERE n.name = {roleName} " + "WITH count(*) AS exists " + "WHERE exists=0 " + "CREATE (m:Role {role}) " + "WITH m " + "MATCH (n:Action) " + "WHERE n.name IN {actions} " + "CREATE UNIQUE m-[:AUTHORIZE]->n " + "RETURN DISTINCT m.id as id"; if (structureId != null && !structureId.trim().isEmpty()) { role.put("structureId", structureId); } JsonObject params = new JsonObject().put("actions", actions) .put("role", role.put("id", UUID.randomUUID().toString())).put("roleName", role.getString("name")); neo.execute(query, params, validUniqueResultHandler(handler)); } @Override public void updateRole(String roleId, JsonObject role, JsonArray actions, Handler<Either<String, JsonObject>> handler) { if (defaultValidationParamsNull(handler, roleId, role, actions)) return; role.remove("id"); String updateValues = ""; if (role.size() > 0) { updateValues = "SET " + nodeSetPropertiesFromJson("role", role); } String updateActions = "RETURN DISTINCT role.id as id"; if (actions.size() > 0) { updateActions = "DELETE r " + "WITH role " + "MATCH (n:Action) " + "WHERE n.name IN {actions} " + "CREATE UNIQUE role-[:AUTHORIZE]->n " + "RETURN DISTINCT role.id as id"; } String query = "MATCH (role:Role {id : {roleId}}) " + "OPTIONAL MATCH role-[r:AUTHORIZE]->(a:Action) " + updateValues + updateActions; role.put("actions", actions).put("roleId", roleId); neo.execute(query, role, validUniqueResultHandler(handler)); } @Override public void deleteRole(String roleId, Handler<Either<String, JsonObject>> handler) { String query = "MATCH (role:Role {id : {id}}) " + "OPTIONAL MATCH role-[r]-() " + "DELETE role, r "; neo.execute(query, new JsonObject().put("id", roleId), validUniqueResultHandler(handler)); } @Override public void linkRolesToGroup(String groupId, JsonArray roleIds, Handler<Either<String, JsonObject>> handler) { JsonObject params = new JsonObject(); params.put("groupId", groupId); if (groupId != null && !groupId.trim().isEmpty()) { String deleteQuery = "MATCH (m:Group)-[r:AUTHORIZED]-(:Role) " + "WHERE m.id = {groupId} " + "DELETE r"; if (roleIds == null || roleIds.size() == 0) { neo.execute(deleteQuery, params, validEmptyHandler(handler)); } else { StatementsBuilder s = new StatementsBuilder().add(deleteQuery, params); String createQuery = "MATCH (n:Role), (m:Group) " + "WHERE m.id = {groupId} AND n.id IN {roles} " + "CREATE UNIQUE m-[:AUTHORIZED]->n"; s.add(createQuery, params.copy().put("roles", roleIds)); neo.executeTransaction(s.build(), null, true, validEmptyHandler(handler)); } } else { handler.handle(new Either.Left<String, JsonObject>("invalid.arguments")); } } @Override public void addGroupLink(String groupId, String roleId, Handler<Either<String, JsonObject>> handler) { JsonObject params = new JsonObject(); params.put("groupId", groupId); params.put("roleId", roleId); if (groupId != null && !groupId.trim().isEmpty() && roleId != null && !roleId.trim().isEmpty()) { String query = "MATCH (r:Role), (g:Group) " + "WHERE r.id = {roleId} and g.id = {groupId} " + "CREATE UNIQUE (g)-[:AUTHORIZED]->(r)"; neo.execute(query, params, Neo4jResult.validEmptyHandler(handler)); } else { handler.handle(new Either.Left<String, JsonObject>("invalid.arguments")); } } @Override public void deleteGroupLink(String groupId, String roleId, Handler<Either<String, JsonObject>> handler) { JsonObject params = new JsonObject(); params.put("groupId", groupId); params.put("roleId", roleId); if (groupId != null && !groupId.trim().isEmpty() && roleId != null && !roleId.trim().isEmpty()) { String query = "MATCH (g:Group)-[auth:AUTHORIZED]->(r:Role) " + "WHERE r.id = {roleId} and g.id = {groupId} " + "DELETE auth"; neo.execute(query, params, Neo4jResult.validEmptyHandler(handler)); } else { handler.handle(new Either.Left<String, JsonObject>("invalid.arguments")); } } @Override public void createApplication(String structureId, JsonObject application, JsonArray actions, final Handler<Either<String, JsonObject>> handler) { if (defaultValidationParamsNull(handler, application, application.getString("name"))) return; final String applicationName = application.getString("name"); final String id = UUID.randomUUID().toString(); final String address = application.getString("address"); application.put("scope", new fr.wseduc.webutils.collections.JsonArray( "[\"" + application.getString("scope", "").replaceAll("\\s", "\",\"") + "\"]")); application.put("id", id); final String createApplicationQuery = "MATCH (n:Application) " + "WHERE n.name = {applicationName} " + "WITH count(*) AS exists " + "WHERE exists=0 " + "CREATE (m:Application {props}) " + "RETURN m.id as id"; final JsonObject params = new JsonObject().put("applicationName", applicationName); if (structureId != null && !structureId.trim().isEmpty()) { application.put("structureId", structureId); } final StatementsBuilder b = new StatementsBuilder().add(createApplicationQuery, params.copy().put("props", application)); if (actions != null && actions.size() > 0) { for (Object o : actions) { JsonObject json = (JsonObject) o; String type; List<String> removeLabels = new ArrayList<>(); removeLabels.add("ResourceAction"); removeLabels.add("AuthenticatedAction"); removeLabels.add("WorkflowAction"); switch (json.getString("type", "WORKFLOW")) { case "RESOURCE": type = "Resource"; break; case "AUTHENTICATED": type = "Authenticated"; break; default: type = "Workflow"; break; } removeLabels.remove(type + "Action"); String createAction = "MERGE (a:Action {name:{name}}) " + "REMOVE a:" + Joiner.on(":").join(removeLabels) + " " + "SET a.displayName = {displayName}, a.type = {type}, a:" + type + "Action " + "WITH a " + "MATCH (n:Application) " + "WHERE n.name = {applicationName} " + "CREATE UNIQUE n-[r:PROVIDE]->a " + "RETURN a.name as name"; b.add(createAction, json.put("applicationName", applicationName).put("type", "SECURED_ACTION_" + json.getString("type", "WORKFLOW"))); } final String removeNotWorkflowInRole = "MATCH (:Role)-[r:AUTHORIZE]->(a:Action) " + "WHERE a:ResourceAction OR a:AuthenticatedAction " + "DELETE r"; b.add(removeNotWorkflowInRole); } else if (address != null && !address.trim().isEmpty()) { String query2 = "MATCH (n:Application) " + "WHERE n.id = {id} " + "CREATE UNIQUE n-[r:PROVIDE]->(a:Action:WorkflowAction {type: {type}, " + "name:{name}, displayName:{displayName}}) " + "RETURN a.name as name"; b.add(query2, new JsonObject().put("id", id).put("type", "SECURED_ACTION_WORKFLOW") .put("name", applicationName + "|address").put("displayName", applicationName + ".address")); } neo.executeTransaction(b.build(), null, true, new Handler<Message<JsonObject>>() { @Override public void handle(Message<JsonObject> m) { JsonArray results = m.body().getJsonArray("results"); if ("ok".equals(m.body().getString("status")) && results != null) { JsonArray r = results.getJsonArray(0); JsonObject j; if (r.size() > 0) { j = r.getJsonObject(0); } else { j = new JsonObject(); } handler.handle(new Either.Right<String, JsonObject>(j)); } else { handler.handle(new Either.Left<String, JsonObject>(m.body().getString("message"))); } } }); } @Override public void getApplication(String applicationId, final Handler<Either<String, JsonObject>> handler) { String query = "MATCH (n:Application) " + "WHERE n.id = {id} " + "RETURN n.id as id, n.name as name, " + "n.grantType as grantType, n.secret as secret, n.address as address, " + "n.icon as icon, n.target as target, n.displayName as displayName, " + "n.scope as scope, n.pattern as pattern, n.casType as casType"; JsonObject params = new JsonObject().put("id", applicationId); neo.execute(query, params, new Handler<Message<JsonObject>>() { @Override public void handle(Message<JsonObject> res) { JsonArray r = res.body().getJsonArray("result"); if (r != null && r.size() == 1) { JsonObject j = r.getJsonObject(0); JsonArray scope = j.getJsonArray("scope"); if (scope != null && scope.size() > 0) { j.put("scope", Joiner.on(" ").join(scope)); } else { j.put("scope", ""); } } handler.handle(validUniqueResult(res)); } }); } @Override public void updateApplication(String applicationId, JsonObject application, Handler<Either<String, JsonObject>> handler) { String query = "MATCH (n:Application) " + "WHERE n.id = {applicationId} AND coalesce(n.locked, false) = false " + "SET " + nodeSetPropertiesFromJson("n", application) + "RETURN n.id as id"; application.put("applicationId", applicationId); application.put("scope", new fr.wseduc.webutils.collections.JsonArray( "[\"" + application.getString("scope", "").replaceAll("\\s", "\",\"") + "\"]")); neo.execute(query, application, validUniqueResultHandler(handler)); } @Override public void deleteApplication(String applicationId, Handler<Either<String, JsonObject>> handler) { String query = "MATCH (n:Application { id : {id}}) " + "WHERE coalesce(n.locked, false) = false " + "OPTIONAL MATCH n-[r1:PROVIDE]->(a:Action) " + "OPTIONAL MATCH a<-[r2:AUTHORIZE]-(r:Role) " + "DELETE n, r1 " + "WITH a, r2 " + "WHERE NOT(a<-[:PROVIDE]-()) " + "DELETE a, r2"; JsonObject params = new JsonObject().put("id", applicationId); neo.execute(query, params, validEmptyHandler(handler)); } @Override public void applicationAllowedUsers(String application, JsonArray users, JsonArray groups, Handler<Either<String, JsonArray>> handler) { JsonObject params = new JsonObject().put("application", application); String filter = ""; if (users != null) { filter += "AND u.id IN {users} "; params.put("users", users); } if (groups != null) { filter += "AND pg.id IN {groups} "; params.put("groups", groups); } String query = "MATCH (app:Application)-[:PROVIDE]->(a:Action)<-[:AUTHORIZE]-(r:Role)" + "<-[:AUTHORIZED]-(pg:Group)<-[:IN]-(u:User) " + "WHERE app.name = {application} " + filter + "RETURN DISTINCT u.id as id"; neo.execute(query, params, validResultHandler(handler)); } @Override public void applicationAllowedProfileGroups(String application, Handler<Either<String, JsonArray>> handler) { String query = "MATCH (app:Application)-[:PROVIDE]->(a:Action)<-[:AUTHORIZE]-(r:Role)" + "<-[:AUTHORIZED]-(g:ProfileGroup)<-[:DEPENDS*0..1]-(pg:ProfileGroup) " + "WHERE app.name = {application} " + "RETURN pg.id as id"; neo.execute(query, new JsonObject().put("application", application), validResultHandler(handler)); } @Override public void setDefaultClassRoles(String classId, Handler<Either<String, JsonObject>> handler) { if (defaultValidationParamsNull(handler, classId)) return; String query = "MATCH (c:Class { id : {id}})<-[:DEPENDS]-(csg:ProfileGroup)-[:DEPENDS]->(ssg:ProfileGroup)-[:HAS_PROFILE]->(sp:Profile {name : 'Student'}), " + "c<-[:DEPENDS]-(ctg:ProfileGroup)-[:DEPENDS]->(stg:ProfileGroup)-[:HAS_PROFILE]->(tp:Profile {name : 'Teacher'}), " + "c<-[:DEPENDS]-(crg:ProfileGroup)-[:DEPENDS]->(srg:ProfileGroup)-[:HAS_PROFILE]->(rp:Profile {name : 'Relative'}), " + "c<-[:DEPENDS]-(cpg:ProfileGroup)-[:DEPENDS]->(spg:ProfileGroup)-[:HAS_PROFILE]->(pp:Profile {name : 'Personnel'}), " + "(rs:Role), (rt:Role), (rr:Role), (pr:Role) " + "WHERE rs.name =~ '^[A-Za-z0-9]+-(student|all)-default$' " + "AND rt.name =~ '^[A-Za-z0-9]+-(teacher|all)-default$' " + "AND rr.name =~ '^[A-Za-z0-9]+-(relative|all)-default$' " + "AND pr.name =~ '^[A-Za-z0-9]+-(personnel|all)-default$' " + "CREATE UNIQUE csg-[:AUTHORIZED]->rs, ctg-[:AUTHORIZED]->rt, crg-[:AUTHORIZED]->rr, cpg-[:AUTHORIZED]->pr"; final JsonObject params = new JsonObject().put("id", classId); final String widgetQuery = "MATCH (c:Class { id : {id}})<-[:DEPENDS]-(csg:ProfileGroup)-[:DEPENDS]->(ssg:ProfileGroup), (w:Widget) " + "WHERE w.default = true " + "MERGE w<-[r:AUTHORIZED]-ssg"; StatementsBuilder sb = new StatementsBuilder(); sb.add(query, params).add(widgetQuery, params); neo.executeTransaction(sb.build(), null, true, validEmptyHandler(handler)); } @Override public void listCasConnectors(final Handler<Either<String, JsonArray>> handler) { String query = "MATCH (app:Application) " + "WHERE has(app.casType) and app.casType <> '' " + "RETURN app.casType as service, app.address as address, COLLECT(app.pattern) as patterns"; neo.execute(query, (JsonObject) null, validResultHandler(new Handler<Either<String, JsonArray>>() { public void handle(Either<String, JsonArray> event) { if (event.isLeft()) { handler.handle(event); return; } JsonArray results = event.right().getValue(); for (Object o : results) { JsonObject app = (JsonObject) o; String address = app.getString("address", ""); JsonArray patterns = app.getJsonArray("patterns", new fr.wseduc.webutils.collections.JsonArray()); if (patterns.size() == 0 || patterns.size() > 0 && patterns.getString(0).isEmpty()) { final URL addressURL = checkCasUrl(address); if (addressURL != null) { String pattern = "^\\Q" + addressURL.getProtocol() + "://" + addressURL.getHost() + (addressURL.getPort() > 0 ? ":" + addressURL.getPort() : "") + "\\E.*"; patterns.add(pattern); } else { log.error("Url for registered service : " + app.getString("service", "") + " is malformed : " + address); } } } handler.handle(new Either.Right<String, JsonArray>(results)); } })); } public static URL checkCasUrl(final String address) { URL addressURL = null; if (!StringUtils.isEmpty(address)) { try { String finalAddress = ""; if (address.startsWith("/adapter#")) { finalAddress = address.substring(address.indexOf("#") + 1); } else if (address.startsWith("/cas/login?service=")) { final String urlEncoded = address.substring(address.indexOf("=") + 1); final String urlDecoded = URLDecoder.decode(urlEncoded, "UTF-8"); //check url is encoded if (!StringUtils.isEmpty(urlDecoded) && !urlDecoded.equals(urlEncoded)) { finalAddress = urlDecoded; } } else { finalAddress = address; } if (!StringUtils.isEmpty(finalAddress)) { final Matcher matcher = URL_PATTERN.matcher(finalAddress); if (matcher.matches()) { addressURL = new URL(finalAddress); } } } catch (MalformedURLException | UnsupportedEncodingException | IllegalArgumentException e) { if (log.isDebugEnabled()) log.debug("address of external CAS app is malformed", e); } } return (addressURL != null && !StringUtils.isEmpty(addressURL.getHost())) ? addressURL : null; } }