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