View Javadoc
1   package de.dlr.shepard.auth.permission.services;
2   
3   import de.dlr.shepard.auth.permission.daos.PermissionsDAO;
4   import de.dlr.shepard.auth.permission.io.PermissionsIO;
5   import de.dlr.shepard.auth.permission.model.Permissions;
6   import de.dlr.shepard.auth.permission.model.Roles;
7   import de.dlr.shepard.auth.users.entities.User;
8   import de.dlr.shepard.auth.users.entities.UserGroup;
9   import de.dlr.shepard.auth.users.services.UserGroupService;
10  import de.dlr.shepard.auth.users.services.UserService;
11  import de.dlr.shepard.common.exceptions.InvalidAuthException;
12  import de.dlr.shepard.common.exceptions.InvalidRequestException;
13  import de.dlr.shepard.common.exceptions.ShepardProcessingException;
14  import de.dlr.shepard.common.neo4j.entities.BasicEntity;
15  import de.dlr.shepard.common.util.AccessType;
16  import de.dlr.shepard.common.util.Constants;
17  import de.dlr.shepard.common.util.PermissionType;
18  import io.quarkus.cache.Cache;
19  import io.quarkus.cache.CacheName;
20  import io.quarkus.cache.CacheResult;
21  import io.quarkus.cache.CompositeCacheKey;
22  import io.quarkus.logging.Log;
23  import jakarta.enterprise.context.RequestScoped;
24  import jakarta.inject.Inject;
25  import jakarta.ws.rs.NotFoundException;
26  import jakarta.ws.rs.container.ContainerRequestContext;
27  import jakarta.ws.rs.core.PathSegment;
28  import java.util.ArrayList;
29  import java.util.HashSet;
30  import java.util.List;
31  import java.util.Optional;
32  import java.util.Set;
33  import org.apache.commons.lang3.StringUtils;
34  
35  @RequestScoped
36  public class PermissionsService {
37  
38    @Inject
39    @CacheName("permissions-service-cache")
40    Cache cache;
41  
42    @Inject
43    PermissionsDAO permissionsDAO;
44  
45    @Inject
46    UserService userService;
47  
48    @Inject
49    UserGroupService userGroupService;
50  
51    /**
52     * @param entity         the entity the permissions belong to
53     * @param user           the user creating the permissions
54     * @param permissionType the initial permission type
55     * @return the newly created permissions
56     */
57    public Permissions createPermissions(BasicEntity entity, User user, PermissionType permissionType) {
58      return permissionsDAO.createOrUpdate(new Permissions(entity, user, PermissionType.Private));
59    }
60  
61    /**
62     * Searches for permissions in Neo4j.
63     * <p>
64     * This function does NOT perform a check if the user is allowed to query the permissions of an entity.
65     *
66     * @param entityId identifies the entity that the permissions object belongs to
67     * @return Optional<Permissions> with matching entity
68     */
69    public Optional<Permissions> getPermissionsOfEntityOptional(long entityId) {
70      var permissions = permissionsDAO.findByEntityNeo4jId(entityId);
71      if (permissions == null) {
72        Log.errorf("Permissions with entity id %s is null", entityId);
73        return Optional.empty();
74      }
75      return Optional.of(permissions);
76    }
77  
78    /**
79     * Searches for permissions in Neo4j.
80     * <p>
81     * This function does perform a check if the user is allowed to query the permissions of an entity.
82     *
83     * @param entityId identifies the entity that the permissions object belongs to
84     * @return Permissions with matching entity
85     * @throws NotFoundException    if permission could not be found
86     * @throws InvalidAuthException if user has to rights to read permissions
87     */
88    public Permissions getPermissionsOfEntity(long entityId) {
89      User user = userService.getCurrentUser();
90      isAccessTypeAllowedForUser(entityId, AccessType.Manage, user.getUsername());
91      return getPermissionsOfEntityOptional(entityId).orElseThrow(() ->
92        new NotFoundException(String.format("Permissions with entity %s is null", entityId))
93      );
94    }
95  
96    /**
97     * @param entityId identifies the entity on which the user has the roles
98     * @param username the user whose roles are checked
99     * @return an object describing the roles of the user on the entity
100    */
101   public Roles getUserRolesOnEntity(long entityId, String username) {
102     var perms = getPermissionsOfEntityOptional(entityId);
103     return getRoles(perms, username);
104   }
105 
106   /**
107    * Check whether a request is allowed or not
108    *
109    * @param entityId   the entity that is to be accessed
110    * @param accessType the access type (read, write, manage)
111    * @param username   the user that wants access
112    * @return whether the access is allowed or not
113    */
114   @CacheResult(cacheName = "permissions-service-cache")
115   public boolean isAccessTypeAllowedForUser(long entityId, AccessType accessType, String username) {
116     Roles userRolesOnEntity = getUserRolesOnEntity(entityId, username);
117 
118     if (userRolesOnEntity.isOwner()) {
119       return true;
120     } else {
121       return switch (accessType) {
122         case Read -> userRolesOnEntity.isReader() || userRolesOnEntity.isWriter() || userRolesOnEntity.isManager();
123         case Write -> userRolesOnEntity.isWriter() || userRolesOnEntity.isManager();
124         case Manage -> userRolesOnEntity.isManager();
125         case None -> false;
126       };
127     }
128   }
129 
130   /**
131    * Checks if the current user is owner of the object specified by its entity id.
132    *
133    * @param entityId
134    * @return boolean, true if current user is owner
135    * @throws InvalidRequestException if user could not be extracted from authentication context
136    */
137   public boolean isCurrentUserOwner(long entityId) {
138     Roles roles = getUserRolesOnEntity(entityId, userService.getCurrentUser().getUsername());
139     return roles.isOwner();
140   }
141 
142   private void removeEntityFromCache(long entityId) {
143     if (cache != null) cache
144       .invalidateIf(el -> ((CompositeCacheKey) el).getKeyElements()[0].equals(entityId))
145       .await()
146       .indefinitely();
147   }
148 
149   /**
150    * Updates the Permissions in Neo4j
151    *
152    * @param permissionsIo the new Permissions object
153    * @param id            identifies the entity
154    * @return the updated Permissions object
155    */
156   public Permissions updatePermissionsByNeo4jId(PermissionsIO permissionsIo, long id) {
157     Permissions oldPermissions = getPermissionsOfEntityOptional(id).orElseThrow(() ->
158       new ShepardProcessingException("Entity has no permissions attached and they cannot be updated!")
159     );
160     var newPermissions = convertPermissionsIO(permissionsIo);
161 
162     if (
163       newPermissions.getOwner() == null ||
164       newPermissions.getOwner().getUniqueId().equals(oldPermissions.getOwner().getUniqueId())
165     ) {
166       oldPermissions.setOwner(oldPermissions.getOwner());
167     } else {
168       if (!isOwner(oldPermissions, userService.getCurrentUser().getUsername())) {
169         throw new InvalidAuthException("Action not allowed. Only Owners are allowed to change ownership.");
170       }
171       // check that new owner actually exists
172       userService.getUser(newPermissions.getOwner().getUsername());
173       oldPermissions.setOwner(newPermissions.getOwner());
174     }
175 
176     oldPermissions.setReader(newPermissions.getReader());
177     oldPermissions.setWriter(newPermissions.getWriter());
178     oldPermissions.setReaderGroups(newPermissions.getReaderGroups());
179     oldPermissions.setWriterGroups(newPermissions.getWriterGroups());
180     oldPermissions.setManager(newPermissions.getManager());
181     oldPermissions.setPermissionType(newPermissions.getPermissionType());
182     var res = permissionsDAO.createOrUpdate(oldPermissions);
183     removeEntityFromCache(id);
184     return res;
185   }
186 
187   public boolean deletePermissions(Permissions permissions) {
188     return permissionsDAO.deleteByNeo4jId(permissions.getId());
189   }
190 
191   /**
192    * Checks if the request is allowed based on access type and user name. The check is performed by checking the path segments, and request body.
193    *
194    * @param requestContext
195    * @param accessType
196    * @param userName
197    */
198   public boolean isAllowed(ContainerRequestContext requestContext, AccessType accessType, String userName) {
199     List<PathSegment> pathSegments = requestContext.getUriInfo().getPathSegments();
200     var idSegment = pathSegments.size() > 1 ? pathSegments.get(1).getPath() : null;
201 
202     // migration state endpoints
203     if (pathSegments.get(0).getPath().equals("temp") && pathSegments.get(1).getPath().equals("migrations")) {
204       return true;
205     }
206 
207     // Paths with length 1
208     if (idSegment == null || idSegment.isBlank()) {
209       // No id in path
210       return true;
211     }
212 
213     // lab journal entries
214     if (pathSegments.get(0).getPath().equals(Constants.LAB_JOURNAL_ENTRIES)) {
215       // Lab journal permissions are already checked inside LabJournalEntryService
216       return true;
217     }
218 
219     // users, apiKeys, subscriptions
220     if (pathSegments.get(0).getPath().equals(Constants.USERS)) {
221       // Permissions are already checked inside User- ApiKey- and SubscriptionService
222       return true;
223     }
224 
225     // entity paths
226     if (StringUtils.isNumeric(idSegment)) {
227       var entityId = Long.parseLong(idSegment);
228       return isAccessTypeAllowedForUser(entityId, accessType, userName);
229     }
230 
231     // usersearch and containersearch
232     if (
233       pathSegments.get(0).getPath().equals(Constants.SEARCH) &&
234       List.of(Constants.USERS, Constants.CONTAINERS, Constants.COLLECTIONS, Constants.USERGROUPS).contains(
235         pathSegments.get(1).getPath()
236       ) &&
237       pathSegments.size() == 2
238     ) {
239       return true;
240     }
241 
242     return false;
243   }
244 
245   private Set<String> fetchUserNames(List<UserGroup> userGroups) {
246     Set<String> ret = new HashSet<>();
247     for (UserGroup userGroup : userGroups) {
248       Optional<UserGroup> fullUserGroup = userGroupService.getUserGroupOptional(userGroup.getId());
249       if (fullUserGroup.isPresent()) {
250         for (User user : fullUserGroup.get().getUsers()) {
251           ret.add(user.getUsername());
252         }
253       }
254     }
255     return ret;
256   }
257 
258   private Roles getRoles(Optional<Permissions> perms, String username) {
259     if (perms.isEmpty()) {
260       // Legacy entity without permissions
261       return new Roles(false, true, true, true);
262     }
263     var roles = new Roles(
264       isOwner(perms.get(), username),
265       isManager(perms.get(), username),
266       isWriter(perms.get(), username),
267       isReader(perms.get(), username)
268     );
269     return roles;
270   }
271 
272   private boolean isOwner(Permissions perms, String username) {
273     return perms.getOwner() != null && username.equals(perms.getOwner().getUsername());
274   }
275 
276   private boolean isManager(Permissions perms, String username) {
277     return perms.getManager().stream().anyMatch(u -> username.equals(u.getUsername()));
278   }
279 
280   private boolean isReader(Permissions perms, String username) {
281     var pub = PermissionType.Public.equals(perms.getPermissionType());
282     var pubRead = PermissionType.PublicReadable.equals(perms.getPermissionType());
283     var reader = perms.getReader().stream().anyMatch(u -> username.equals(u.getUsername()));
284     var readerGroup = fetchUserNames(perms.getReaderGroups()).contains(username);
285     return pub || pubRead || reader || readerGroup;
286   }
287 
288   private boolean isWriter(Permissions perms, String username) {
289     var pub = PermissionType.Public.equals(perms.getPermissionType());
290     var writer = perms.getWriter().stream().anyMatch(u -> username.equals(u.getUsername()));
291     var writerGroup = fetchUserNames(perms.getWriterGroups()).contains(username);
292     return pub || writer || writerGroup;
293   }
294 
295   /**
296    * Fetches all missing data and transforms PermissionsIO to a Permissions object.
297    */
298   private Permissions convertPermissionsIO(PermissionsIO permissions) {
299     var owner = permissions.getOwner() != null
300       ? userService.getUserOptional(permissions.getOwner()).orElseGet(null)
301       : null;
302     var permissionType = permissions.getPermissionType();
303     var reader = fetchUsers(permissions.getReader());
304     var writer = fetchUsers(permissions.getWriter());
305     var readerGroups = fetchUserGroups(permissions.getReaderGroupIds());
306     var writerGroups = fetchUserGroups(permissions.getWriterGroupIds());
307     var manager = fetchUsers(permissions.getManager());
308     return new Permissions(owner, reader, writer, readerGroups, writerGroups, manager, permissionType);
309   }
310 
311   private List<User> fetchUsers(String[] usernames) {
312     var result = new ArrayList<User>(usernames.length);
313     for (var username : usernames) {
314       if (username == null) {
315         continue;
316       }
317 
318       Optional<User> user = userService.getUserOptional(username);
319       user.ifPresent((User u) -> result.add(u));
320     }
321     return result;
322   }
323 
324   private List<UserGroup> fetchUserGroups(long[] userGroupIds) {
325     var result = new ArrayList<UserGroup>(userGroupIds.length);
326     for (var userGroupId : userGroupIds) {
327       Optional<UserGroup> userGroup = userGroupService.getUserGroupOptional(userGroupId);
328       if (userGroup.isPresent()) {
329         result.add(userGroup.get());
330       }
331     }
332     return result;
333   }
334 }