View Javadoc

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         // If a user creates an item, grant them OWNER perms on it.
219         // Owner permissions do not have a scope.
220         // FIXME: Currently a hack here so this doesn't apply to admin
221         // users - but it probably should...
222         if (!AclManager.belongsToAdmin(accessor)) {
223             aclManager.withScope(SystemScope.INSTANCE)
224                     .grantPermission(item, PermissionType.OWNER, accessor);
225         }
226         // If the scope is not the system, set the permission scope
227         // of the item too...
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                 // Log the action...
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                 // Log the action...
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                 // Log the action...
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                 // TODO: This should be rather more complicated and account for the
321                 // fact that individual grants can, in theory, have more than one
322                 // target content type.
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                 // Log the action...
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                 // Log the action...
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         // TODO: Should this require perms to link another item???
366         //helper.checkEntityPermission(t2, user, PermissionType.ANNOTATE);
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         // TODO: Should this require perms to link another item???
395         //helper.checkEntityPermission(t2, user, PermissionType.ANNOTATE);
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             // FIXME: Better error message here...
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     // Helpers...
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      * Check the accessor has GRANT permissions to update another user's
543      * permissions.
544      *
545      * @param accessor      the user
546      * @param permissionSet the user's permissions.
547      */
548     private void checkGrantPermission(Accessor accessor,
549             GlobalPermissionSet permissionSet)
550             throws PermissionDenied {
551         Map<ContentTypes, Collection<PermissionType>> permissionMap
552                 = permissionSet.asMap();
553         // Check we have grant permissions for the requested content types
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         // If a user is not admin they can only add someone else to a group if
582         // a) they belong to that group themselves, and
583         // b) they have the modify permission on that group, and
584         // c) they have grant permissions for the user
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 }