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