1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one
3  * or more contributor license agreements.  See the NOTICE file
4  * distributed with this work for additional information
5  * regarding copyright ownership.  The ASF licenses this file
6  * to you under the Apache License, Version 2.0 (the
7  * "License"); you may not use this file except in compliance
8  * with the License.  You may obtain a copy of the License at
9  *
10  *   http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing,
13  * software distributed under the License is distributed on an
14  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15  * KIND, either express or implied.  See the License for the
16  * specific language governing permissions and limitations
17  * under the License.
18  */
19 
20 package org.apache.guacamole.auth.jdbc.base;
21 
22 import java.util.ArrayList;
23 import java.util.Collection;
24 import java.util.Collections;
25 import java.util.Set;
26 import org.apache.guacamole.auth.jdbc.user.ModeledAuthenticatedUser;
27 import org.apache.guacamole.GuacamoleException;
28 import org.apache.guacamole.GuacamoleSecurityException;
29 import org.apache.guacamole.auth.jdbc.permission.ObjectPermissionMapper;
30 import org.apache.guacamole.auth.jdbc.permission.ObjectPermissionModel;
31 import org.apache.guacamole.auth.jdbc.user.UserModel;
32 import org.apache.guacamole.net.auth.Identifiable;
33 import org.apache.guacamole.net.auth.permission.ObjectPermission;
34 import org.apache.guacamole.net.auth.permission.ObjectPermissionSet;
35 import org.mybatis.guice.transactional.Transactional;
36 
37 /**
38  * Service which provides convenience methods for creating, retrieving, and
39  * manipulating objects within directories. This service will automatically
40  * enforce the permissions of the current user.
41  *
42  * @param <InternalType>
43  *     The specific internal implementation of the type of object this service
44  *     provides access to.
45  *
46  * @param <ExternalType>
47  *     The external interface or implementation of the type of object this
48  *     service provides access to, as defined by the guacamole-ext API.
49  *
50  * @param <ModelType>
51  *     The underlying model object used to represent InternalType in the
52  *     database.
53  */
54 public abstract class ModeledDirectoryObjectService<InternalType extends ModeledDirectoryObject<ModelType>,
55         ExternalType extends Identifiable, ModelType extends ObjectModel>
56     implements DirectoryObjectService<InternalType, ExternalType> {
57 
58     /**
59      * All object permissions which are implicitly granted upon creation to the
60      * creator of the object.
61      */
62     private static final ObjectPermission.Type[] IMPLICIT_OBJECT_PERMISSIONS = {
63         ObjectPermission.Type.READ,
64         ObjectPermission.Type.UPDATE,
65         ObjectPermission.Type.DELETE,
66         ObjectPermission.Type.ADMINISTER
67     };
68 
69     /**
70      * Returns an instance of a mapper for the type of object used by this
71      * service.
72      *
73      * @return
74      *     A mapper which provides access to the model objects associated with
75      *     the objects used by this service.
76      */
getObjectMapper()77     protected abstract ModeledDirectoryObjectMapper<ModelType> getObjectMapper();
78 
79     /**
80      * Returns an instance of a mapper for the type of permissions that affect
81      * the type of object used by this service.
82      *
83      * @return
84      *     A mapper which provides access to the model objects associated with
85      *     the permissions that affect the objects used by this service.
86      */
getPermissionMapper()87     protected abstract ObjectPermissionMapper getPermissionMapper();
88 
89     /**
90      * Returns an instance of an object which is backed by the given model
91      * object.
92      *
93      * @param currentUser
94      *     The user for whom this object is being created.
95      *
96      * @param model
97      *     The model object to use to back the returned object.
98      *
99      * @return
100      *     An object which is backed by the given model object.
101      *
102      * @throws GuacamoleException
103      *     If the object instance cannot be created.
104      */
getObjectInstance(ModeledAuthenticatedUser currentUser, ModelType model)105     protected abstract InternalType getObjectInstance(ModeledAuthenticatedUser currentUser,
106             ModelType model) throws GuacamoleException;
107 
108     /**
109      * Returns an instance of a model object which is based on the given
110      * object.
111      *
112      * @param currentUser
113      *     The user for whom this model object is being created.
114      *
115      * @param object
116      *     The object to use to produce the returned model object.
117      *
118      * @return
119      *     A model object which is based on the given object.
120      *
121      * @throws GuacamoleException
122      *     If the model object instance cannot be created.
123      */
getModelInstance(ModeledAuthenticatedUser currentUser, ExternalType object)124     protected abstract ModelType getModelInstance(ModeledAuthenticatedUser currentUser,
125             ExternalType object) throws GuacamoleException;
126 
127     /**
128      * Returns whether the given user has permission to create the type of
129      * objects that this directory object service manages, taking into account
130      * permission inheritance through user groups.
131      *
132      * @param user
133      *     The user being checked.
134      *
135      * @return
136      *     true if the user has object creation permission relevant to this
137      *     directory object service, false otherwise.
138      *
139      * @throws GuacamoleException
140      *     If permission to read the user's permissions is denied.
141      */
hasCreatePermission(ModeledAuthenticatedUser user)142     protected abstract boolean hasCreatePermission(ModeledAuthenticatedUser user)
143             throws GuacamoleException;
144 
145     /**
146      * Returns whether the given user has permission to perform a certain
147      * action on a specific object managed by this directory object service,
148      * taking into account permission inheritance through user groups.
149      *
150      * @param user
151      *     The user being checked.
152      *
153      * @param identifier
154      *     The identifier of the object to check.
155      *
156      * @param type
157      *     The type of action that will be performed.
158      *
159      * @return
160      *     true if the user has object permission relevant described, false
161      *     otherwise.
162      *
163      * @throws GuacamoleException
164      *     If permission to read the user's permissions is denied.
165      */
hasObjectPermission(ModeledAuthenticatedUser user, String identifier, ObjectPermission.Type type)166     protected boolean hasObjectPermission(ModeledAuthenticatedUser user,
167             String identifier, ObjectPermission.Type type)
168             throws GuacamoleException {
169 
170         // Get object permissions
171         ObjectPermissionSet permissionSet = getEffectivePermissionSet(user);
172 
173         // Return whether permission is granted
174         return user.isPrivileged()
175             || permissionSet.hasPermission(type, identifier);
176 
177     }
178 
179     /**
180      * Returns the permission set associated with the given user and related
181      * to the type of objects handled by this directory object service, taking
182      * into account permission inheritance via user groups.
183      *
184      * @param user
185      *     The user whose permissions are being retrieved.
186      *
187      * @return
188      *     A permission set which contains the permissions associated with the
189      *     given user and related to the type of objects handled by this
190      *     directory object service.
191      *
192      * @throws GuacamoleException
193      *     If permission to read the user's permissions is denied.
194      */
getEffectivePermissionSet(ModeledAuthenticatedUser user)195     protected abstract ObjectPermissionSet getEffectivePermissionSet(ModeledAuthenticatedUser user)
196             throws GuacamoleException;
197 
198     /**
199      * Returns a collection of objects which are backed by the models in the
200      * given collection.
201      *
202      * @param currentUser
203      *     The user for whom these objects are being created.
204      *
205      * @param models
206      *     The model objects to use to back the objects within the returned
207      *     collection.
208      *
209      * @return
210      *     A collection of objects which are backed by the models in the given
211      *     collection.
212      *
213      * @throws GuacamoleException
214      *     If any of the object instances cannot be created.
215      */
getObjectInstances(ModeledAuthenticatedUser currentUser, Collection<ModelType> models)216     protected Collection<InternalType> getObjectInstances(ModeledAuthenticatedUser currentUser,
217             Collection<ModelType> models) throws GuacamoleException {
218 
219         // Create new collection of objects by manually converting each model
220         Collection<InternalType> objects = new ArrayList<InternalType>(models.size());
221         for (ModelType model : models)
222             objects.add(getObjectInstance(currentUser, model));
223 
224         return objects;
225 
226     }
227 
228     /**
229      * Called before any object is created through this directory object
230      * service. This function serves as a final point of validation before
231      * the create operation occurs. In its default implementation,
232      * beforeCreate() performs basic permissions checks.
233      *
234      * @param user
235      *     The user creating the object.
236      *
237      * @param object
238      *     The object being created.
239      *
240      * @param model
241      *     The model of the object being created.
242      *
243      * @throws GuacamoleException
244      *     If the object is invalid, or an error prevents validating the given
245      *     object.
246      */
beforeCreate(ModeledAuthenticatedUser user, ExternalType object, ModelType model)247     protected void beforeCreate(ModeledAuthenticatedUser user,
248             ExternalType object, ModelType model) throws GuacamoleException {
249 
250         // Verify permission to create objects
251         if (!user.isPrivileged() && !hasCreatePermission(user))
252             throw new GuacamoleSecurityException("Permission denied.");
253 
254     }
255 
256     /**
257      * Called before any object is updated through this directory object
258      * service. This function serves as a final point of validation before
259      * the update operation occurs. In its default implementation,
260      * beforeUpdate() performs basic permissions checks.
261      *
262      * @param user
263      *     The user updating the existing object.
264      *
265      * @param object
266      *     The object being updated.
267      *
268      * @param model
269      *     The model of the object being updated.
270      *
271      * @throws GuacamoleException
272      *     If the object is invalid, or an error prevents validating the given
273      *     object.
274      */
beforeUpdate(ModeledAuthenticatedUser user, InternalType object, ModelType model)275     protected void beforeUpdate(ModeledAuthenticatedUser user,
276             InternalType object, ModelType model) throws GuacamoleException {
277 
278         // By default, do nothing.
279         if (!hasObjectPermission(user, model.getIdentifier(), ObjectPermission.Type.UPDATE))
280             throw new GuacamoleSecurityException("Permission denied.");
281 
282     }
283 
284     /**
285      * Called before any object is deleted through this directory object
286      * service. This function serves as a final point of validation before
287      * the delete operation occurs. In its default implementation,
288      * beforeDelete() performs basic permissions checks.
289      *
290      * @param user
291      *     The user deleting the existing object.
292      *
293      * @param identifier
294      *     The identifier of the object being deleted.
295      *
296      * @throws GuacamoleException
297      *     If the object is invalid, or an error prevents validating the given
298      *     object.
299      */
beforeDelete(ModeledAuthenticatedUser user, String identifier)300     protected void beforeDelete(ModeledAuthenticatedUser user,
301             String identifier) throws GuacamoleException {
302 
303         // Verify permission to delete objects
304         if (!hasObjectPermission(user, identifier, ObjectPermission.Type.DELETE))
305             throw new GuacamoleSecurityException("Permission denied.");
306 
307     }
308 
309     /**
310      * Returns whether the given string is a valid identifier within the JDBC
311      * authentication extension. Invalid identifiers may result in SQL errors
312      * from the underlying database when used in queries.
313      *
314      * @param identifier
315      *     The string to check for validity.
316      *
317      * @return
318      *     true if the given string is a valid identifier, false otherwise.
319      */
isValidIdentifier(String identifier)320     protected boolean isValidIdentifier(String identifier) {
321 
322         // Empty identifiers are invalid
323         if (identifier.isEmpty())
324             return false;
325 
326         // Identifier is invalid if any non-numeric characters are present
327         for (int i = 0; i < identifier.length(); i++) {
328             if (!Character.isDigit(identifier.charAt(i)))
329                 return false;
330         }
331 
332         // Identifier is valid - contains only numeric characters
333         return true;
334 
335     }
336 
337     /**
338      * Filters the given collection of strings, returning a new collection
339      * containing only those strings which are valid identifiers. If no strings
340      * within the collection are valid identifiers, the returned collection will
341      * simply be empty.
342      *
343      * @param identifiers
344      *     The collection of strings to filter.
345      *
346      * @return
347      *     A new collection containing only the strings within the provided
348      *     collection which are valid identifiers.
349      */
filterIdentifiers(Collection<String> identifiers)350     protected Collection<String> filterIdentifiers(Collection<String> identifiers) {
351 
352         // Obtain enough space for a full copy of the given identifiers
353         Collection<String> validIdentifiers = new ArrayList<String>(identifiers.size());
354 
355         // Add only valid identifiers to the copy
356         for (String identifier : identifiers) {
357             if (isValidIdentifier(identifier))
358                 validIdentifiers.add(identifier);
359         }
360 
361         return validIdentifiers;
362 
363     }
364 
365     @Override
retrieveObject(ModeledAuthenticatedUser user, String identifier)366     public InternalType retrieveObject(ModeledAuthenticatedUser user,
367             String identifier) throws GuacamoleException {
368 
369         // Pull objects having given identifier
370         Collection<InternalType> objects = retrieveObjects(user, Collections.singleton(identifier));
371 
372         // If no such object, return null
373         if (objects.isEmpty())
374             return null;
375 
376         // The object collection will have exactly one element unless the
377         // database has seriously lost integrity
378         assert(objects.size() == 1);
379 
380         // Return first and only object
381         return objects.iterator().next();
382 
383     }
384 
385     @Override
retrieveObjects(ModeledAuthenticatedUser user, Collection<String> identifiers)386     public Collection<InternalType> retrieveObjects(ModeledAuthenticatedUser user,
387             Collection<String> identifiers) throws GuacamoleException {
388 
389         // Ignore invalid identifiers
390         identifiers = filterIdentifiers(identifiers);
391 
392         // Do not query if no identifiers given
393         if (identifiers.isEmpty())
394             return Collections.<InternalType>emptyList();
395 
396         Collection<ModelType> objects;
397 
398         // Bypass permission checks if the user is privileged
399         if (user.isPrivileged())
400             objects = getObjectMapper().select(identifiers);
401 
402         // Otherwise only return explicitly readable identifiers
403         else
404             objects = getObjectMapper().selectReadable(user.getUser().getModel(),
405                     identifiers, user.getEffectiveUserGroups());
406 
407         // Return collection of requested objects
408         return getObjectInstances(user, objects);
409 
410     }
411 
412     /**
413      * Returns an immutable collection of permissions that should be granted due
414      * to the creation of the given object. These permissions need not be
415      * granted solely to the user creating the object.
416      *
417      * @param user
418      *     The user creating the object.
419      *
420      * @param model
421      *     The object being created.
422      *
423      * @return
424      *     The collection of implicit permissions that should be granted due to
425      *     the creation of the given object.
426      */
getImplicitPermissions(ModeledAuthenticatedUser user, ModelType model)427     protected Collection<ObjectPermissionModel> getImplicitPermissions(ModeledAuthenticatedUser user,
428             ModelType model) {
429 
430         // Check to see if the user granting permissions is a skeleton user,
431         // thus lacking database backing.
432         if (user.getUser().isSkeleton())
433             return Collections.emptyList();
434 
435         // Build list of implicit permissions
436         Collection<ObjectPermissionModel> implicitPermissions =
437                 new ArrayList<>(IMPLICIT_OBJECT_PERMISSIONS.length);
438 
439 
440         UserModel userModel = user.getUser().getModel();
441         for (ObjectPermission.Type permission : IMPLICIT_OBJECT_PERMISSIONS) {
442 
443             // Create model which grants this permission to the current user
444             ObjectPermissionModel permissionModel = new ObjectPermissionModel();
445             permissionModel.setEntityID(userModel.getEntityID());
446             permissionModel.setType(permission);
447             permissionModel.setObjectIdentifier(model.getIdentifier());
448 
449             // Add permission
450             implicitPermissions.add(permissionModel);
451 
452         }
453 
454         return Collections.unmodifiableCollection(implicitPermissions);
455 
456     }
457 
458     @Override
459     @Transactional
createObject(ModeledAuthenticatedUser user, ExternalType object)460     public InternalType createObject(ModeledAuthenticatedUser user, ExternalType object)
461         throws GuacamoleException {
462 
463         ModelType model = getModelInstance(user, object);
464         beforeCreate(user, object, model);
465 
466         // Create object
467         getObjectMapper().insert(model);
468 
469         // Set identifier on original object
470         object.setIdentifier(model.getIdentifier());
471 
472         // Add implicit permissions
473         Collection<ObjectPermissionModel> implicitPermissions = getImplicitPermissions(user, model);
474         if (!implicitPermissions.isEmpty())
475             getPermissionMapper().insert(implicitPermissions);
476 
477         // Add any arbitrary attributes
478         if (model.hasArbitraryAttributes())
479             getObjectMapper().insertAttributes(model);
480 
481         return getObjectInstance(user, model);
482 
483     }
484 
485     @Override
deleteObject(ModeledAuthenticatedUser user, String identifier)486     public void deleteObject(ModeledAuthenticatedUser user, String identifier)
487         throws GuacamoleException {
488 
489         beforeDelete(user, identifier);
490 
491         // Delete object
492         getObjectMapper().delete(identifier);
493 
494     }
495 
496     @Override
497     @Transactional
updateObject(ModeledAuthenticatedUser user, InternalType object)498     public void updateObject(ModeledAuthenticatedUser user, InternalType object)
499         throws GuacamoleException {
500 
501         ModelType model = object.getModel();
502         beforeUpdate(user, object, model);
503 
504         // Update object
505         getObjectMapper().update(model);
506 
507         // Replace any existing arbitrary attributes
508         getObjectMapper().deleteAttributes(model);
509         if (model.hasArbitraryAttributes())
510             getObjectMapper().insertAttributes(model);
511 
512     }
513 
514     @Override
getIdentifiers(ModeledAuthenticatedUser user)515     public Set<String> getIdentifiers(ModeledAuthenticatedUser user)
516         throws GuacamoleException {
517 
518         // Bypass permission checks if the user is privileged
519         if (user.isPrivileged())
520             return getObjectMapper().selectIdentifiers();
521 
522         // Otherwise only return explicitly readable identifiers
523         else
524             return getObjectMapper().selectReadableIdentifiers(user.getUser().getModel(),
525                     user.getEffectiveUserGroups());
526 
527     }
528 
529 }
530