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