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