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