001package de.deepamehta.core.impl; 002 003import de.deepamehta.core.Association; 004import de.deepamehta.core.DeepaMehtaObject; 005import de.deepamehta.core.RelatedTopic; 006import de.deepamehta.core.Topic; 007import de.deepamehta.core.model.AssociationModel; 008import de.deepamehta.core.model.SimpleValue; 009import de.deepamehta.core.model.RelatedTopicModel; 010import de.deepamehta.core.model.TopicModel; 011import de.deepamehta.core.service.ModelFactory; 012import de.deepamehta.core.service.accesscontrol.AccessControl; 013import de.deepamehta.core.service.accesscontrol.Credentials; 014import de.deepamehta.core.service.accesscontrol.Operation; 015import de.deepamehta.core.service.accesscontrol.SharingMode; 016 017import javax.servlet.http.HttpServletRequest; 018import javax.servlet.http.HttpSession; 019 020import java.util.List; 021import java.util.concurrent.Callable; 022import java.util.logging.Logger; 023 024 025 026class AccessControlImpl implements AccessControl { 027 028 // ------------------------------------------------------------------------------------------------------- Constants 029 030 // Type URIs 031 // ### TODO: move to dm4.core namespace? 032 // ### TODO: copy in AccessControlPlugin.java 033 private static final String TYPE_MEMBERSHIP = "dm4.accesscontrol.membership"; 034 private static final String TYPE_USERNAME = "dm4.accesscontrol.username"; 035 // 036 private static final String TYPE_EMAIL_ADDRESS = "dm4.contacts.email_address"; 037 // ### TODO: copy in ConfigPlugin.java 038 private static final String ASSOC_TYPE_CONFIGURATION = "dm4.config.configuration"; 039 private static final String ROLE_TYPE_CONFIGURABLE = "dm4.config.configurable"; 040 private static final String ROLE_TYPE_DEFAULT = "dm4.core.default"; 041 042 // Property URIs 043 // ### TODO: move to dm4.core namespace? 044 // ### TODO: copy in AccessControlPlugin.java 045 private static final String PROP_CREATOR = "dm4.accesscontrol.creator"; 046 // ### TODO: copy in AccessControlPlugin.java 047 private static final String PROP_OWNER = "dm4.accesscontrol.owner"; 048 // ### TODO: copy in WorkspacesPlugin.java 049 private static final String PROP_WORKSPACE_ID = "dm4.workspaces.workspace_id"; 050 051 // Workspace URIs 052 // ### TODO: copy in WorkspaceService.java 053 private static final String DEEPAMEHTA_WORKSPACE_URI = "dm4.workspaces.deepamehta"; 054 // ### TODO: copy in AccessControlService.java 055 private static final String ADMINISTRATION_WORKSPACE_URI = "dm4.workspaces.administration"; 056 private static final String SYSTEM_WORKSPACE_URI = "dm4.workspaces.system"; 057 058 private long systemWorkspaceId = -1; // initialized lazily 059 060 // ---------------------------------------------------------------------------------------------- Instance Variables 061 062 // standard workspace assignment suppression 063 private ThreadLocal<Integer> suppressionLevel = new ThreadLocal() { 064 @Override 065 protected Integer initialValue() { 066 return 0; 067 } 068 }; 069 070 private PersistenceLayer pl; 071 private ModelFactory mf; 072 073 private Logger logger = Logger.getLogger(getClass().getName()); 074 075 // ---------------------------------------------------------------------------------------------------- Constructors 076 077 AccessControlImpl(PersistenceLayer pl) { 078 this.pl = pl; 079 this.mf = pl.mf; 080 } 081 082 // -------------------------------------------------------------------------------------------------- Public Methods 083 084 @Override 085 public Topic checkCredentials(Credentials cred) { 086 TopicModel usernameTopic = null; 087 try { 088 usernameTopic = _getUsernameTopic(cred.username); 089 if (usernameTopic == null) { 090 return null; 091 } 092 if (!matches(usernameTopic, cred.password)) { 093 return null; 094 } 095 return instantiate(usernameTopic); 096 } catch (Exception e) { 097 throw new RuntimeException("Checking credentials for user \"" + cred.username + 098 "\" failed (usernameTopic=" + usernameTopic + ")", e); 099 } 100 } 101 102 @Override 103 public boolean hasPermission(String username, Operation operation, long objectId) { 104 String typeUri = null; 105 try { 106 typeUri = getTypeUri(objectId); 107 // 108 // Note: private topicmaps are treated special. The topicmap's workspace assignment doesn't matter here. 109 // Also "operation" doesn't matter as READ/WRITE access is always granted/denied together. 110 if (typeUri.equals("dm4.topicmaps.topicmap") && isTopicmapPrivate(objectId)) { 111 return isCreator(username, objectId); 112 } 113 // 114 long workspaceId; 115 if (typeUri.equals("dm4.workspaces.workspace")) { 116 workspaceId = objectId; 117 } else { 118 workspaceId = getAssignedWorkspaceId(objectId); 119 if (workspaceId == -1) { 120 // fallback when no workspace is assigned 121 return permissionIfNoWorkspaceIsAssigned(operation, objectId, typeUri); 122 } 123 } 124 // 125 return _hasPermission(username, operation, workspaceId); 126 } catch (Exception e) { 127 throw new RuntimeException("Checking permission for object " + objectId + " failed (typeUri=\"" + typeUri + 128 "\", " + userInfo(username) + ", operation=" + operation + ")", e); 129 } 130 } 131 132 @Override 133 public String getCreator(long objectId) { 134 return pl.hasProperty(objectId, PROP_CREATOR) ? (String) pl.fetchProperty(objectId, PROP_CREATOR) : null; 135 } 136 137 138 139 // === Workspaces / Memberships === 140 141 @Override 142 public Topic getWorkspace(String uri) { 143 Topic workspace = pl.getTopicByUri(uri); 144 if (workspace == null) { 145 throw new RuntimeException("Workspace \"" + uri + "\" does not exist"); 146 } 147 return workspace; 148 } 149 150 // --- 151 152 @Override 153 public long getDeepaMehtaWorkspaceId() { 154 return getWorkspace(DEEPAMEHTA_WORKSPACE_URI).getId(); 155 } 156 157 @Override 158 public long getAdministrationWorkspaceId() { 159 return getWorkspace(ADMINISTRATION_WORKSPACE_URI).getId(); 160 } 161 162 @Override 163 public long getSystemWorkspaceId() { 164 if (systemWorkspaceId == -1) { 165 // Note: fetching the System workspace topic though the Core service would involve a permission check 166 // and run in a vicious circle. So direct storage access is required here. 167 TopicModel workspace = fetchTopic("uri", SYSTEM_WORKSPACE_URI); 168 // Note: the Access Control plugin creates the System workspace before it performs its first permission 169 // check. 170 if (workspace == null) { 171 throw new RuntimeException("The System workspace does not exist"); 172 } 173 // 174 systemWorkspaceId = workspace.getId(); 175 } 176 return systemWorkspaceId; 177 } 178 179 // --- 180 181 @Override 182 public long getAssignedWorkspaceId(long objectId) { 183 long workspaceId = -1; 184 try { 185 if (pl.hasProperty(objectId, PROP_WORKSPACE_ID)) { 186 workspaceId = (Long) pl.fetchProperty(objectId, PROP_WORKSPACE_ID); 187 checkWorkspaceId(workspaceId); 188 } 189 return workspaceId; 190 } catch (Exception e) { 191 throw new RuntimeException("Object " + objectId + " is assigned to workspace " + workspaceId, e); 192 } 193 } 194 195 @Override 196 public void assignToWorkspace(DeepaMehtaObject object, long workspaceId) { 197 try { 198 // create assignment association 199 pl.createAssociation("dm4.core.aggregation", 200 object.getModel().createRoleModel("dm4.core.parent"), 201 mf.newTopicRoleModel(workspaceId, "dm4.core.child") 202 ); 203 // store assignment property 204 object.setProperty(PROP_WORKSPACE_ID, workspaceId, true); // addToIndex=true 205 } catch (Exception e) { 206 throw new RuntimeException("Assigning " + object + " to workspace " + workspaceId + " failed", e); 207 } 208 } 209 210 @Override 211 public boolean isWorkspaceAssignment(Association assoc) { 212 if (assoc.getTypeUri().equals("dm4.core.aggregation")) { 213 TopicModel topic = ((AssociationModelImpl) assoc.getModel()).getTopic("dm4.core.child"); 214 if (topic != null && topic.getTypeUri().equals("dm4.workspaces.workspace")) { 215 return true; 216 } 217 } 218 return false; 219 } 220 221 // --- 222 223 @Override 224 public <V> V runWithoutWorkspaceAssignment(Callable<V> callable) throws Exception { 225 int level = suppressionLevel.get(); 226 try { 227 suppressionLevel.set(level + 1); 228 return callable.call(); // throws exception 229 } finally { 230 suppressionLevel.set(level); 231 } 232 } 233 234 @Override 235 public boolean workspaceAssignmentIsSuppressed() { 236 return suppressionLevel.get() > 0; 237 } 238 239 240 241 // === User Accounts === 242 243 @Override 244 public Topic getUsernameTopic(String username) { 245 TopicModel usernameTopic = _getUsernameTopic(username); 246 return usernameTopic != null ? instantiate(usernameTopic) : null; 247 } 248 249 @Override 250 public Topic getUsernameTopic(HttpServletRequest request) { 251 String username = getUsername(request); 252 if (username == null) { 253 return null; 254 } 255 return instantiate(_getUsernameTopicOrThrow(username)); 256 } 257 258 @Override 259 public String getUsername(HttpServletRequest request) { 260 try { 261 HttpSession session = request.getSession(false); // create=false 262 if (session == null) { 263 return null; 264 } 265 return username(session); 266 } catch (IllegalStateException e) { 267 // Note: this happens if request is a proxy object (injected by Jersey) and a request method is called 268 // outside request scope. This is the case while system startup. 269 return null; // user is unknown 270 } 271 } 272 273 @Override 274 public String username(HttpSession session) { 275 String username = (String) session.getAttribute("username"); 276 if (username == null) { 277 throw new RuntimeException("Session data inconsistency: \"username\" attribute is missing"); 278 } 279 return username; 280 } 281 282 // --- 283 284 @Override 285 public Topic getPrivateWorkspace(String username) { 286 TopicModel passwordTopic = getPasswordTopic(_getUsernameTopicOrThrow(username)); 287 long workspaceId = getAssignedWorkspaceId(passwordTopic.getId()); 288 if (workspaceId == -1) { 289 throw new RuntimeException("User \"" + username + "\" has no private workspace"); 290 } 291 return instantiate(pl.fetchTopic(workspaceId)); 292 } 293 294 @Override 295 public boolean isMember(String username, long workspaceId) { 296 try { 297 if (username == null) { 298 return false; 299 } 300 // Note: direct storage access is required here 301 AssociationModel membership = pl.fetchAssociation(TYPE_MEMBERSHIP, 302 _getUsernameTopicOrThrow(username).getId(), workspaceId, "dm4.core.default", "dm4.core.default"); 303 return membership != null; 304 } catch (Exception e) { 305 throw new RuntimeException("Checking membership of user \"" + username + "\" and workspace " + 306 workspaceId + " failed", e); 307 } 308 } 309 310 311 312 // === Config Service === 313 314 @Override 315 public RelatedTopic getConfigTopic(String configTypeUri, long topicId) { 316 try { 317 RelatedTopicModel configTopic = pl.fetchTopicRelatedTopic(topicId, ASSOC_TYPE_CONFIGURATION, 318 ROLE_TYPE_CONFIGURABLE, ROLE_TYPE_DEFAULT, configTypeUri); 319 if (configTopic == null) { 320 throw new RuntimeException("The \"" + configTypeUri + "\" configuration topic for topic " + topicId + 321 " is missing"); 322 } 323 return new RelatedTopicImpl((RelatedTopicModelImpl) configTopic, pl); 324 } catch (Exception e) { 325 throw new RuntimeException("Getting the \"" + configTypeUri + "\" configuration topic for topic " + 326 topicId + " failed", e); 327 } 328 } 329 330 331 332 // === Email Addresses === 333 334 @Override 335 public boolean emailAddressExists(String emailAddress) { 336 return !queryTopics(TYPE_EMAIL_ADDRESS, emailAddress).isEmpty(); 337 } 338 339 340 341 // ------------------------------------------------------------------------------------------------- Private Methods 342 343 /** 344 * Prerequisite: usernameTopic is not <code>null</code>. 345 * 346 * @param password The encoded password. 347 */ 348 private boolean matches(TopicModel usernameTopic, String password) { 349 String _password = getPasswordTopic(usernameTopic).getSimpleValue().toString(); // encoded 350 return _password.equals(password); 351 } 352 353 /** 354 * Prerequisite: usernameTopic is not <code>null</code>. 355 */ 356 private TopicModel getPasswordTopic(TopicModel usernameTopic) { 357 return _getPasswordTopic(_getUserAccount(usernameTopic)); 358 } 359 360 /** 361 * Prerequisite: usernameTopic is not <code>null</code>. 362 */ 363 private TopicModel _getUserAccount(TopicModel usernameTopic) { 364 // Note: checking the credentials is performed by <anonymous> and User Accounts are private. 365 // So direct storage access is required here. 366 RelatedTopicModel userAccount = pl.fetchTopicRelatedTopic(usernameTopic.getId(), "dm4.core.composition", 367 "dm4.core.child", "dm4.core.parent", "dm4.accesscontrol.user_account"); 368 if (userAccount == null) { 369 throw new RuntimeException("Data inconsistency: there is no User Account topic for username \"" + 370 usernameTopic.getSimpleValue() + "\" (usernameTopic=" + usernameTopic + ")"); 371 } 372 return userAccount; 373 } 374 375 /** 376 * Prerequisite: userAccount is not <code>null</code>. 377 */ 378 private TopicModel _getPasswordTopic(TopicModel userAccount) { 379 // Note: we only have a (User Account) topic model at hand and we don't want instantiate a Topic. 380 // So we use direct storage access here. 381 RelatedTopicModel password = pl.fetchTopicRelatedTopic(userAccount.getId(), "dm4.core.composition", 382 "dm4.core.parent", "dm4.core.child", "dm4.accesscontrol.password"); 383 if (password == null) { 384 throw new RuntimeException("Data inconsistency: there is no Password topic for User Account \"" + 385 userAccount.getSimpleValue() + "\" (userAccount=" + userAccount + ")"); 386 } 387 return password; 388 } 389 390 // --- 391 392 // ### TODO: remove this workaround 393 private boolean permissionIfNoWorkspaceIsAssigned(Operation operation, long objectId, String typeUri) { 394 switch (operation) { 395 case READ: 396 logger.fine("Object " + objectId + " (typeUri=\"" + typeUri + 397 "\") is not assigned to any workspace -- READ permission is granted"); 398 return true; 399 case WRITE: 400 logger.warning("Object " + objectId + " (typeUri=\"" + typeUri + 401 "\") is not assigned to any workspace -- WRITE permission is refused"); 402 return false; 403 default: 404 throw new RuntimeException(operation + " is an unsupported operation"); 405 } 406 } 407 408 private boolean _hasPermission(String username, Operation operation, long workspaceId) { 409 switch (operation) { 410 case READ: 411 return hasReadPermission(username, workspaceId); 412 case WRITE: 413 return hasWritePermission(username, workspaceId); 414 default: 415 throw new RuntimeException(operation + " is an unsupported operation"); 416 } 417 } 418 419 // --- 420 421 /** 422 * @param username the logged in user, or <code>null</code> if no user is logged in. 423 * @param workspaceId the ID of the workspace that is relevant for the permission check. Is never -1. 424 */ 425 private boolean hasReadPermission(String username, long workspaceId) { 426 SharingMode sharingMode = getSharingMode(workspaceId); 427 switch (sharingMode) { 428 case PRIVATE: 429 return isOwner(username, workspaceId); 430 case CONFIDENTIAL: 431 return isOwner(username, workspaceId) || isMember(username, workspaceId); 432 case COLLABORATIVE: 433 return isOwner(username, workspaceId) || isMember(username, workspaceId); 434 case PUBLIC: 435 // Note: the System workspace is treated special: although it is a public workspace 436 // its content is readable only for logged in users. 437 return workspaceId != getSystemWorkspaceId() || username != null; 438 case COMMON: 439 return true; 440 default: 441 throw new RuntimeException(sharingMode + " is an unsupported sharing mode"); 442 } 443 } 444 445 /** 446 * @param username the logged in user, or <code>null</code> if no user is logged in. 447 * @param workspaceId the ID of the workspace that is relevant for the permission check. Is never -1. 448 */ 449 private boolean hasWritePermission(String username, long workspaceId) { 450 SharingMode sharingMode = getSharingMode(workspaceId); 451 switch (sharingMode) { 452 case PRIVATE: 453 return isOwner(username, workspaceId); 454 case CONFIDENTIAL: 455 return isOwner(username, workspaceId); 456 case COLLABORATIVE: 457 return isOwner(username, workspaceId) || isMember(username, workspaceId); 458 case PUBLIC: 459 return isOwner(username, workspaceId) || isMember(username, workspaceId); 460 case COMMON: 461 return true; 462 default: 463 throw new RuntimeException(sharingMode + " is an unsupported sharing mode"); 464 } 465 } 466 467 // --- 468 469 /** 470 * Checks if a user is the owner of a workspace. 471 * 472 * @param username the logged in user, or <code>null</code> if no user is logged in. 473 * 474 * @return <code>true</code> if the user is the owner, <code>false</code> otherwise. 475 */ 476 private boolean isOwner(String username, long workspaceId) { 477 try { 478 if (username == null) { 479 return false; 480 } 481 return getOwner(workspaceId).equals(username); 482 } catch (Exception e) { 483 throw new RuntimeException("Checking ownership of workspace " + workspaceId + " and user \"" + 484 username + "\" failed", e); 485 } 486 } 487 488 private SharingMode getSharingMode(long workspaceId) { 489 // Note: direct storage access is required here 490 TopicModel sharingMode = pl.fetchTopicRelatedTopic(workspaceId, "dm4.core.aggregation", "dm4.core.parent", 491 "dm4.core.child", "dm4.workspaces.sharing_mode"); 492 if (sharingMode == null) { 493 throw new RuntimeException("No sharing mode is assigned to workspace " + workspaceId); 494 } 495 return SharingMode.fromString(sharingMode.getUri()); 496 } 497 498 private void checkWorkspaceId(long workspaceId) { 499 String typeUri = getTypeUri(workspaceId); 500 if (!typeUri.equals("dm4.workspaces.workspace")) { 501 throw new RuntimeException("Object " + workspaceId + " is not a workspace (but of type \"" + typeUri + 502 "\")"); 503 } 504 } 505 506 // --- 507 508 private boolean isTopicmapPrivate(long topicmapId) { 509 TopicModel privateFlag = pl.fetchTopicRelatedTopic(topicmapId, "dm4.core.composition", "dm4.core.parent", 510 "dm4.core.child", "dm4.topicmaps.private"); 511 if (privateFlag == null) { 512 // Note: migrated topicmaps might not have a Private child topic ### TODO: throw exception? 513 return false; // treat as non-private 514 } 515 return privateFlag.getSimpleValue().booleanValue(); 516 } 517 518 private boolean isCreator(String username, long objectId) { 519 return username != null ? username.equals(getCreator(objectId)) : false; 520 } 521 522 // --- 523 524 private String getOwner(long workspaceId) { 525 // Note: direct storage access is required here 526 if (!pl.hasProperty(workspaceId, PROP_OWNER)) { 527 throw new RuntimeException("No owner is assigned to workspace " + workspaceId); 528 } 529 return (String) pl.fetchProperty(workspaceId, PROP_OWNER); 530 } 531 532 private String getTypeUri(long objectId) { 533 // Note: direct storage access is required here 534 return (String) pl.fetchProperty(objectId, "type_uri"); 535 } 536 537 // --- 538 539 private TopicModel _getUsernameTopic(String username) { 540 // Note: username topics are not readable by <anonymous>. 541 // So direct storage access is required here. 542 return fetchTopic(TYPE_USERNAME, username); 543 } 544 545 private TopicModel _getUsernameTopicOrThrow(String username) { 546 TopicModel usernameTopic = _getUsernameTopic(username); 547 if (usernameTopic == null) { 548 throw new RuntimeException("User \"" + username + "\" does not exist"); 549 } 550 return usernameTopic; 551 } 552 553 // --- 554 555 /** 556 * Fetches a topic by key/value via direct storage access. 557 * <p> 558 * IMPORTANT: only applicable to values indexed with <code>dm4.core.key</code>. 559 * 560 * @return the topic, or <code>null</code> if no such topic exists. 561 */ 562 private TopicModel fetchTopic(String key, Object value) { 563 return pl.fetchTopic(key, new SimpleValue(value)); 564 } 565 566 /** 567 * Queries topics by key/value via direct storage access. 568 * <p> 569 * IMPORTANT: only applicable to values indexed with <code>dm4.core.fulltext</code> or 570 * <code>dm4.core.fulltext_key</code>. 571 * 572 * @return a list, possibly empty. 573 */ 574 private List<TopicModelImpl> queryTopics(String key, Object value) { 575 return pl.queryTopics(key, new SimpleValue(value)); 576 } 577 578 // --- 579 580 /** 581 * Instantiates a topic without performing permission check. 582 */ 583 private Topic instantiate(TopicModel model) { 584 return new TopicImpl((TopicModelImpl) model, pl); 585 } 586 587 588 589 // === Logging === 590 591 // ### TODO: there is a copy in AccessControlPlugin.java 592 private String userInfo(String username) { 593 return "user " + (username != null ? "\"" + username + "\"" : "<anonymous>"); 594 } 595}