1 package eu.ehri.project.api.impl;
2
3 import com.google.common.collect.Sets;
4 import com.tinkerpop.frames.FramedGraph;
5 import eu.ehri.project.acl.AclManager;
6 import eu.ehri.project.acl.ContentTypes;
7 import eu.ehri.project.acl.GlobalPermissionSet;
8 import eu.ehri.project.acl.InheritedGlobalPermissionSet;
9 import eu.ehri.project.acl.InheritedItemPermissionSet;
10 import eu.ehri.project.acl.PermissionType;
11 import eu.ehri.project.acl.PermissionUtils;
12 import eu.ehri.project.acl.SystemScope;
13 import eu.ehri.project.api.Api;
14 import eu.ehri.project.api.ConceptsApi;
15 import eu.ehri.project.api.EventsApi;
16 import eu.ehri.project.api.QueryApi;
17 import eu.ehri.project.api.UserProfilesApi;
18 import eu.ehri.project.api.VirtualUnitsApi;
19 import eu.ehri.project.core.GraphManager;
20 import eu.ehri.project.core.GraphManagerFactory;
21 import eu.ehri.project.definitions.EventTypes;
22 import eu.ehri.project.definitions.Ontology;
23 import eu.ehri.project.exceptions.AccessDenied;
24 import eu.ehri.project.exceptions.DeserializationError;
25 import eu.ehri.project.exceptions.ItemNotFound;
26 import eu.ehri.project.exceptions.PermissionDenied;
27 import eu.ehri.project.exceptions.SerializationError;
28 import eu.ehri.project.exceptions.ValidationError;
29 import eu.ehri.project.models.AccessPoint;
30 import eu.ehri.project.models.AccessPointType;
31 import eu.ehri.project.models.Annotation;
32 import eu.ehri.project.models.EntityClass;
33 import eu.ehri.project.models.Group;
34 import eu.ehri.project.models.Link;
35 import eu.ehri.project.models.Permission;
36 import eu.ehri.project.models.PermissionGrant;
37 import eu.ehri.project.models.UserProfile;
38 import eu.ehri.project.models.base.Accessible;
39 import eu.ehri.project.models.base.Accessor;
40 import eu.ehri.project.models.base.Actioner;
41 import eu.ehri.project.models.base.Annotatable;
42 import eu.ehri.project.models.base.Described;
43 import eu.ehri.project.models.base.Description;
44 import eu.ehri.project.models.base.Entity;
45 import eu.ehri.project.models.base.Linkable;
46 import eu.ehri.project.models.base.PermissionGrantTarget;
47 import eu.ehri.project.models.base.PermissionScope;
48 import eu.ehri.project.models.base.Promotable;
49 import eu.ehri.project.persistence.ActionManager;
50 import eu.ehri.project.persistence.Bundle;
51 import eu.ehri.project.persistence.BundleManager;
52 import eu.ehri.project.persistence.Mutation;
53 import eu.ehri.project.persistence.Serializer;
54 import eu.ehri.project.persistence.VersionManager;
55
56 import java.util.Collection;
57 import java.util.List;
58 import java.util.Map;
59 import java.util.Optional;
60 import java.util.Set;
61
62 public class ApiImpl implements Api {
63
64 private final FramedGraph<?> graph;
65 private final Accessor accessor;
66 private final PermissionScope scope;
67 private final boolean logging;
68 private final GraphManager manager;
69 private final PermissionUtils helper;
70 private final AclManager aclManager;
71 private final ActionManager actionManager;
72 private final VersionManager versionManager;
73 private final VirtualUnitsApiImpl virtualUnitViews;
74 private final UserProfilesApiImpl userProfilesApi;
75 private final ConceptsApi conceptsApi;
76 private final BundleManager bundleManager;
77 private final Serializer depSerializer;
78 private final EventsApi eventsApi;
79
80 public ApiImpl(FramedGraph<?> graph, Accessor accessor, PermissionScope scope, boolean logging) {
81 this.graph = graph;
82 this.accessor = accessor;
83 this.scope = Optional.ofNullable(scope).orElse(SystemScope.getInstance());
84 this.logging = logging;
85 this.manager = GraphManagerFactory.getInstance(graph);
86 this.helper = new PermissionUtils(graph, scope);
87 this.aclManager = new AclManager(graph, scope);
88 this.actionManager = new ActionManager(graph, scope);
89 this.versionManager = new VersionManager(graph);
90 this.virtualUnitViews = new VirtualUnitsApiImpl(graph, accessor);
91 this.conceptsApi = new ConceptsApiImpl(graph, accessor, logging);
92 this.userProfilesApi = new UserProfilesApiImpl(graph, this);
93 this.eventsApi = new EventsApiImpl(graph, accessor);
94 this.bundleManager = new BundleManager(graph);
95 this.depSerializer = new Serializer.Builder(graph).dependentOnly().build();
96 }
97
98 @Override
99 public Accessor accessor() {
100 return accessor;
101 }
102
103 @Override
104 public boolean isLogging() {
105 return logging;
106 }
107
108 @Override
109 public ActionManager actionManager() {
110 return actionManager;
111 }
112
113 @Override
114 public VersionManager versionManager() {
115 return versionManager;
116 }
117
118 @Override
119 public VirtualUnitsApi virtualUnits() {
120 return virtualUnitViews;
121 }
122
123 @Override
124 public UserProfilesApi userProfiles() {
125 return userProfilesApi;
126 }
127
128 @Override
129 public ConceptsApi concepts() {
130 return conceptsApi;
131 }
132
133 @Override
134 public EventsApi events() {
135 return eventsApi;
136 }
137
138 @Override
139 public QueryApi query() {
140 return new QueryApiImpl(graph, accessor);
141 }
142
143 @Override
144 public Serializer serializer() {
145 return new Serializer(graph);
146 }
147
148 @Override
149 public Api withAccessor(Accessor accessor) {
150 return new ApiImpl(graph, accessor, scope, logging);
151 }
152
153 @Override
154 public Api withScope(PermissionScope scope) {
155 return new ApiImpl(graph, accessor, Optional.ofNullable(scope)
156 .orElse(SystemScope.getInstance()), logging);
157 }
158
159 @Override
160 public Api enableLogging(boolean logEnabled) {
161 return new ApiImpl(graph, accessor, scope, logEnabled);
162 }
163
164 @Override
165 public <E extends Accessible> E detail(String id, Class<E> cls) throws ItemNotFound {
166 E item = manager.getEntity(id, cls);
167 if (!aclManager.canAccess(item, accessor)) {
168 throw new ItemNotFound(id);
169 }
170 return item;
171 }
172
173
174 @Override
175 public <E extends Accessible> Mutation<E> update(Bundle bundle, Class<E> cls)
176 throws PermissionDenied, ValidationError, ItemNotFound, DeserializationError {
177 return update(bundle, cls, Optional.<String>empty());
178 }
179
180 @Override
181 public <E extends Accessible> E create(Bundle bundle, Class<E> cls)
182 throws PermissionDenied, ValidationError, DeserializationError {
183 return create(bundle, cls, Optional.<String>empty());
184 }
185
186 @Override
187 public <E extends Accessible> Mutation<E> createOrUpdate(Bundle bundle, Class<E> cls)
188 throws PermissionDenied, ValidationError, DeserializationError {
189 return createOrUpdate(bundle, cls, Optional.<String>empty());
190 }
191
192 @Override
193 public int delete(String id) throws PermissionDenied, ValidationError, SerializationError, ItemNotFound {
194 return delete(id, Optional.<String>empty());
195 }
196
197 @Override
198 public <E extends Accessible> Mutation<E> update(Bundle bundle, Class<E> cls, Optional<String> logMessage)
199 throws PermissionDenied, ValidationError, ItemNotFound, DeserializationError {
200 E entity = graph.frame(manager.getVertex(bundle.getId()), cls);
201 helper.checkEntityPermission(entity, accessor, PermissionType.UPDATE);
202 Mutation<E> out = bundleManager.withScopeIds(scope.idPath()).update(bundle, cls);
203 if (out.hasChanged()) {
204 commitEvent(actionManager.newEventContext(
205 out.getNode(), accessor.as(Actioner.class),
206 EventTypes.modification, logMessage)
207 .createVersion(out.getNode(), out.getPrior().get()));
208 }
209 return out;
210 }
211
212 @Override
213 public <E extends Accessible> E create(Bundle bundle, Class<E> cls, Optional<String> logMessage)
214 throws PermissionDenied, ValidationError, DeserializationError {
215 helper.checkContentPermission(accessor, helper.getContentTypeEnum(cls),
216 PermissionType.CREATE);
217 E item = bundleManager.withScopeIds(scope.idPath()).create(bundle, cls);
218
219
220
221
222 if (!AclManager.belongsToAdmin(accessor)) {
223 aclManager.withScope(SystemScope.INSTANCE)
224 .grantPermission(item, PermissionType.OWNER, accessor);
225 }
226
227
228 if (!scope.equals(SystemScope.getInstance())) {
229 item.setPermissionScope(scope);
230 }
231
232 commitEvent(actionManager
233 .newEventContext(item, accessor.as(Actioner.class),
234 EventTypes.creation, logMessage));
235 return item;
236
237 }
238
239 @Override
240 public <E extends Accessible> Mutation<E> createOrUpdate(Bundle bundle, Class<E> cls, Optional<String> logMessage)
241 throws PermissionDenied, ValidationError, DeserializationError {
242 helper.checkContentPermission(accessor, helper.getContentTypeEnum(cls),
243 PermissionType.CREATE);
244 helper.checkContentPermission(accessor, helper.getContentTypeEnum(cls),
245 PermissionType.UPDATE);
246 Mutation<E> out = bundleManager.withScopeIds(scope.idPath()).createOrUpdate(bundle, cls);
247 if (out.updated()) {
248 commitEvent(actionManager
249 .newEventContext(out.getNode(), accessor.as(Actioner.class),
250 EventTypes.modification, logMessage)
251 .createVersion(out.getNode(), out.getPrior().get()));
252 }
253 return out;
254 }
255
256 @Override
257 public int delete(String id, Optional<String> logMessage)
258 throws PermissionDenied, ValidationError, SerializationError, ItemNotFound {
259 Accessible item = manager.getEntity(id, Accessible.class);
260 helper.checkEntityPermission(item, accessor, PermissionType.DELETE);
261 commitEvent(actionManager
262 .newEventContext(item, accessor.as(Actioner.class),
263 EventTypes.deletion, logMessage)
264 .createVersion(item));
265 return bundleManager.withScopeIds(scope.idPath())
266 .delete(depSerializer.entityToBundle(item));
267 }
268
269 @Override
270 public AclManager aclManager() {
271 return aclManager;
272 }
273
274 @Override
275 public Acl acl() {
276 return new Acl() {
277 @Override
278 public InheritedGlobalPermissionSet setGlobalPermissionMatrix(Accessor userOrGroup, GlobalPermissionSet permissionSet)
279 throws PermissionDenied {
280 checkGrantPermission(accessor, permissionSet);
281 aclManager.setPermissionMatrix(userOrGroup, permissionSet);
282 boolean scoped = !scope.equals(SystemScope.INSTANCE);
283
284 if (logging) {
285 ActionManager.EventContext context = actionManager.newEventContext(
286 userOrGroup.as(Accessible.class),
287 accessor.as(Actioner.class),
288 EventTypes.setGlobalPermissions);
289 if (scoped) {
290 context.addSubjects(scope.as(Accessible.class));
291 }
292 commitEvent(context);
293 }
294 return aclManager.getInheritedGlobalPermissions(userOrGroup);
295 }
296
297 @Override
298 public void setAccessors(Accessible entity, Set<Accessor> accessors) throws PermissionDenied {
299 helper.checkEntityPermission(entity, accessor, PermissionType.UPDATE);
300 aclManager.setAccessors(entity, accessors);
301
302 commitEvent(actionManager.newEventContext(
303 entity, accessor.as(Actioner.class), EventTypes.setVisibility));
304 }
305
306 @Override
307 public InheritedItemPermissionSet setItemPermissions(Accessible item, Accessor userOrGroup, Set<PermissionType> permissionList)
308 throws PermissionDenied {
309 helper.checkEntityPermission(item, accessor, PermissionType.GRANT);
310 aclManager.setItemPermissions(item, userOrGroup, permissionList);
311
312 commitEvent(actionManager.newEventContext(item,
313 accessor.as(Actioner.class), EventTypes.setItemPermissions)
314 .addSubjects(userOrGroup.as(Accessible.class)));
315 return aclManager.getInheritedItemPermissions(item, userOrGroup);
316 }
317
318 @Override
319 public void revokePermissionGrant(PermissionGrant grant) throws PermissionDenied {
320
321
322
323 for (PermissionGrantTarget tg : grant.getTargets()) {
324 switch (manager.getEntityClass(tg)) {
325 case CONTENT_TYPE:
326 helper.checkContentPermission(accessor, ContentTypes.withName(tg.getId()), PermissionType.GRANT);
327 break;
328 default:
329 helper.checkEntityPermission(
330 tg.as(Accessible.class), accessor, PermissionType.GRANT);
331 }
332 }
333 aclManager.revokePermissionGrant(grant);
334 }
335
336 @Override
337 public void addAccessorToGroup(Group group, Accessor userOrGroup) throws PermissionDenied {
338 ensureCanModifyGroupMembership(group, userOrGroup, accessor);
339 group.addMember(userOrGroup);
340
341 commitEvent(actionManager.newEventContext(group,
342 accessor.as(Actioner.class), EventTypes.addGroup)
343 .addSubjects(userOrGroup.as(Accessible.class)));
344 }
345
346 @Override
347 public void removeAccessorFromGroup(Group group, Accessor userOrGroup) throws PermissionDenied {
348 ensureCanModifyGroupMembership(group, userOrGroup, accessor);
349 group.removeMember(userOrGroup);
350
351 commitEvent(actionManager.newEventContext(group,
352 accessor.as(Actioner.class), EventTypes.removeGroup)
353 .addSubjects(userOrGroup.as(Accessible.class)));
354 }
355 };
356 }
357
358 @Override
359 public Link createLink(String targetId1, String targetId2, List<String> bodies,
360 Bundle bundle, Collection<Accessor> accessibleTo, Optional<String> logMessage)
361 throws ItemNotFound, ValidationError, PermissionDenied {
362 Linkable t1 = manager.getEntity(targetId1, Linkable.class);
363 Linkable t2 = manager.getEntity(targetId2, Linkable.class);
364 helper.checkEntityPermission(t1, accessor, PermissionType.ANNOTATE);
365
366
367 Link link = bundleManager.create(bundle, Link.class);
368 link.addLinkTarget(t1);
369 link.addLinkTarget(t2);
370 link.setLinker(accessor);
371 aclManager.setAccessors(link, accessibleTo);
372 ActionManager.EventContext eventContext = actionManager.setScope(t1).newEventContext(
373 accessor.as(Actioner.class),
374 EventTypes.link, logMessage)
375 .addSubjects(link)
376 .addSubjects(t2);
377 for (String body : bodies) {
378 Accessible item = manager.getEntity(body, Accessible.class);
379 link.addLinkBody(item);
380 eventContext.addSubjects(item);
381 }
382 commitEvent(eventContext);
383 return link;
384 }
385
386 @Override
387 public Link createAccessPointLink(String targetId1, String targetId2, String descriptionId, String bodyName,
388 AccessPointType bodyType, Bundle bundle, Collection<Accessor> accessibleTo, Optional<String> logMessage)
389 throws ItemNotFound, ValidationError, PermissionDenied {
390 Linkable t1 = manager.getEntity(targetId1, Linkable.class);
391 Linkable t2 = manager.getEntity(targetId2, Linkable.class);
392 Description description = manager.getEntity(descriptionId, Description.class);
393 helper.checkEntityPermission(t1, accessor, PermissionType.ANNOTATE);
394
395
396 helper.checkEntityPermission(description.getEntity(), accessor, PermissionType.UPDATE);
397 Link link = bundleManager.create(bundle, Link.class);
398 Bundle relBundle = Bundle.of(EntityClass.ACCESS_POINT)
399 .withDataValue(Ontology.NAME_KEY, bodyName)
400 .withDataValue(Ontology.ACCESS_POINT_TYPE, bodyType)
401 .withDataValue(Ontology.LINK_HAS_DESCRIPTION, link.getDescription());
402 AccessPoint rel = bundleManager.create(relBundle, AccessPoint.class);
403 description.addAccessPoint(rel);
404 link.addLinkTarget(t1);
405 link.addLinkTarget(t2);
406 link.setLinker(accessor);
407 link.addLinkBody(rel);
408 aclManager.setAccessors(link, accessibleTo);
409 ActionManager.EventContext eventContext = actionManager.newEventContext(
410 t1, accessor.as(Actioner.class), EventTypes.link, logMessage);
411 eventContext.addSubjects(link).addSubjects(t2).addSubjects(rel);
412 commitEvent(eventContext);
413 return link;
414 }
415
416 @Override
417 public Annotation createAnnotation(String id, String did, Bundle bundle,
418 Collection<Accessor> accessibleTo, Optional<String> logMessage)
419 throws PermissionDenied, AccessDenied, ValidationError, ItemNotFound {
420 Annotatable entity = manager.getEntity(id, Annotatable.class);
421 Annotatable dep = manager.getEntity(did, Annotatable.class);
422 helper.checkEntityPermission(entity.as(Accessible.class), accessor, PermissionType.ANNOTATE);
423 helper.checkReadAccess(entity.as(Accessible.class), accessor);
424
425 if (!(entity.equals(dep) || isInSubtree(entity, dep))) {
426
427 throw new PermissionDenied("Item is not covered by parent item's permissions");
428 }
429
430 Annotation annotation = bundleManager.create(bundle, Annotation.class);
431 entity.addAnnotation(annotation);
432 if (!entity.equals(dep)) {
433 dep.addAnnotationPart(annotation);
434 }
435 annotation.setAnnotator(accessor.as(UserProfile.class));
436 aclManager.setAccessors(annotation, accessibleTo);
437 aclManager.withScope(SystemScope.INSTANCE)
438 .grantPermission(annotation, PermissionType.OWNER, accessor);
439
440 commitEvent(actionManager.setScope(entity)
441 .newEventContext(annotation,
442 accessor.as(Actioner.class),
443 EventTypes.annotation, logMessage));
444 return annotation;
445 }
446
447 @Override
448 public Promotable promote(String id) throws ItemNotFound, PermissionDenied, NotPromotableError {
449 Promotable item = detail(id, Promotable.class);
450 helper.checkEntityPermission(item, accessor, PermissionType.PROMOTE);
451 if (!item.isPromotable()) {
452 throw new NotPromotableError(item.getId());
453 }
454 UserProfile user = accessor.as(UserProfile.class);
455 item.addPromotion(user);
456 commitEvent(actionManager.newEventContext(item, user, EventTypes.promotion));
457 return item;
458 }
459
460 @Override
461 public Promotable removePromotion(String id) throws ItemNotFound, PermissionDenied {
462 Promotable item = detail(id, Promotable.class);
463 item.removePromotion(accessor.as(UserProfile.class));
464 return item;
465 }
466
467 @Override
468 public Promotable demote(String id) throws ItemNotFound, PermissionDenied, NotPromotableError {
469 Promotable item = detail(id, Promotable.class);
470 helper.checkEntityPermission(item, accessor, PermissionType.PROMOTE);
471 if (!item.isPromotable()) {
472 throw new ApiImpl.NotPromotableError(item.getId());
473 }
474 UserProfile user = accessor.as(UserProfile.class);
475 item.addDemotion(user);
476 commitEvent(actionManager.newEventContext(item, user, EventTypes.demotion));
477 return item;
478 }
479
480 @Override
481 public Promotable removeDemotion(String id) throws ItemNotFound, PermissionDenied {
482 Promotable item = detail(id, Promotable.class);
483 item.removeDemotion(accessor.as(UserProfile.class));
484 return item;
485 }
486
487 @Override
488 public int deleteDependent(String parentId, String id, Optional<String> logMessage)
489 throws ItemNotFound, PermissionDenied, SerializationError {
490 Described parent = detail(parentId, Described.class);
491 Accessible dependentItem = manager.getEntity(id, Accessible.class);
492 if (!itemsInSubtree(parent).contains(dependentItem)) {
493 throw new PermissionDenied("Given description does not belong to its parent item");
494 }
495 helper.checkEntityPermission(parent, accessor, PermissionType.UPDATE);
496 commitEvent(actionManager.newEventContext(parent, accessor.as(Actioner.class),
497 EventTypes.deleteDependent, logMessage)
498 .createVersion(dependentItem));
499 return bundleManager.withScopeIds(parent.idPath())
500 .delete(depSerializer.entityToBundle(dependentItem));
501 }
502
503 @Override
504 public <T extends Accessible> T createDependent(String parentId, Bundle data, Class<T> cls, Optional<String> logMessage)
505 throws ItemNotFound, PermissionDenied, ValidationError {
506 Described parent = detail(parentId, Described.class);
507 helper.checkEntityPermission(parent, accessor, PermissionType.UPDATE);
508 T out = bundleManager.withScopeIds(parent.idPath()).create(data, cls);
509 commitEvent(actionManager.newEventContext(parent, accessor.as(Actioner.class),
510 EventTypes.createDependent, logMessage));
511 return out;
512 }
513
514 @Override
515 public <T extends Accessible> Mutation<T> updateDependent(String parentId, Bundle data, Class<T> cls, Optional<String> logMessage)
516 throws ItemNotFound, PermissionDenied, ValidationError {
517 Described parent = detail(parentId, Described.class);
518 helper.checkEntityPermission(parent, accessor, PermissionType.UPDATE);
519 Mutation<T> out = bundleManager.withScopeIds(parent.idPath()).update(data, cls);
520 if (out.hasChanged()) {
521 commitEvent(actionManager
522 .newEventContext(parent, accessor.as(Actioner.class),
523 EventTypes.modifyDependent, logMessage)
524 .createVersion(out.getNode(), out.getPrior().get()));
525 }
526 return out;
527 }
528
529
530
531 private boolean isInSubtree(Entity parent, Entity child) {
532 return itemsInSubtree(parent).contains(child);
533 }
534
535 private Set<Entity> itemsInSubtree(Entity topLevel) {
536 final Set<Entity> items = Sets.newHashSet();
537 depSerializer.traverseSubtree(topLevel, (frame, depth, relation, relationIndex) -> items.add(frame));
538 return items;
539 }
540
541
542
543
544
545
546
547
548 private void checkGrantPermission(Accessor accessor,
549 GlobalPermissionSet permissionSet)
550 throws PermissionDenied {
551 Map<ContentTypes, Collection<PermissionType>> permissionMap
552 = permissionSet.asMap();
553
554 if (!AclManager.belongsToAdmin(accessor)) {
555 try {
556 Permission grantPerm = manager.getEntity(
557 PermissionType.GRANT.getName(), Permission.class);
558 for (ContentTypes ctype : permissionMap.keySet()) {
559 if (!aclManager.hasPermission(ctype, PermissionType.GRANT, accessor)) {
560 throw new PermissionDenied(accessor.getId(),
561 ctype.toString(), grantPerm.getId(),
562 scope.getId());
563 }
564 }
565 } catch (ItemNotFound e) {
566 throw new RuntimeException(
567 "Unable to get node for permission type '"
568 + PermissionType.GRANT + "'", e);
569 }
570 }
571 }
572
573 private void commitEvent(ActionManager.EventContext context) {
574 if (logging) {
575 context.commit();
576 }
577 }
578
579 private void ensureCanModifyGroupMembership(Group group, Accessor user, Accessor grantee)
580 throws PermissionDenied {
581
582
583
584
585 if (!AclManager.belongsToAdmin(grantee)) {
586 boolean found = false;
587 for (Accessor acc : grantee.getAllParents()) {
588 if (group.equals(acc)) {
589 found = true;
590 break;
591 }
592 }
593 if (!found) {
594 throw new PermissionDenied(grantee.getId(), group.getId(),
595 "Non-admin users cannot add other users to groups that they" +
596 " do not themselves belong to.");
597 }
598 helper.checkEntityPermission(user.as(Accessible.class),
599 grantee, PermissionType.GRANT);
600 helper.checkEntityPermission(group, grantee, PermissionType.UPDATE);
601 }
602 }
603 }