001 package de.deepamehta.plugins.accesscontrol; 002 003 import de.deepamehta.plugins.accesscontrol.event.PostLoginUserListener; 004 import de.deepamehta.plugins.accesscontrol.event.PostLogoutUserListener; 005 import de.deepamehta.plugins.accesscontrol.service.AccessControlService; 006 import de.deepamehta.plugins.workspaces.service.WorkspacesService; 007 008 import de.deepamehta.core.Association; 009 import de.deepamehta.core.AssociationType; 010 import de.deepamehta.core.ChildTopics; 011 import de.deepamehta.core.DeepaMehtaObject; 012 import de.deepamehta.core.RelatedTopic; 013 import de.deepamehta.core.Topic; 014 import de.deepamehta.core.TopicType; 015 import de.deepamehta.core.Type; 016 import de.deepamehta.core.ViewConfiguration; 017 import de.deepamehta.core.model.AssociationModel; 018 import de.deepamehta.core.model.ChildTopicsModel; 019 import de.deepamehta.core.model.SimpleValue; 020 import de.deepamehta.core.model.TopicModel; 021 import de.deepamehta.core.model.TopicRoleModel; 022 import de.deepamehta.core.osgi.PluginActivator; 023 import de.deepamehta.core.service.DeepaMehtaEvent; 024 import de.deepamehta.core.service.EventListener; 025 import de.deepamehta.core.service.Inject; 026 import de.deepamehta.core.service.Transactional; 027 import de.deepamehta.core.service.accesscontrol.AccessControlException; 028 import de.deepamehta.core.service.accesscontrol.Credentials; 029 import de.deepamehta.core.service.accesscontrol.Operation; 030 import de.deepamehta.core.service.accesscontrol.Permissions; 031 import de.deepamehta.core.service.accesscontrol.SharingMode; 032 import de.deepamehta.core.service.event.PostCreateAssociationListener; 033 import de.deepamehta.core.service.event.PostCreateTopicListener; 034 import de.deepamehta.core.service.event.PostUpdateAssociationListener; 035 import de.deepamehta.core.service.event.PostUpdateTopicListener; 036 import de.deepamehta.core.service.event.PreGetAssociationListener; 037 import de.deepamehta.core.service.event.PreGetTopicListener; 038 import de.deepamehta.core.service.event.ResourceRequestFilterListener; 039 import de.deepamehta.core.service.event.ServiceRequestFilterListener; 040 import de.deepamehta.core.storage.spi.DeepaMehtaTransaction; 041 import de.deepamehta.core.util.JavaUtils; 042 043 // ### TODO: hide Jersey internals. Move to JAX-RS 2.0. 044 import com.sun.jersey.spi.container.ContainerRequest; 045 046 import javax.servlet.http.HttpServletRequest; 047 import javax.servlet.http.HttpSession; 048 049 import javax.ws.rs.GET; 050 import javax.ws.rs.PUT; 051 import javax.ws.rs.POST; 052 import javax.ws.rs.DELETE; 053 import javax.ws.rs.Consumes; 054 import javax.ws.rs.Path; 055 import javax.ws.rs.PathParam; 056 import javax.ws.rs.Produces; 057 import javax.ws.rs.WebApplicationException; 058 import javax.ws.rs.core.Context; 059 import javax.ws.rs.core.Response; 060 import javax.ws.rs.core.Response.Status; 061 062 import java.util.Collection; 063 import java.util.Enumeration; 064 import java.util.logging.Logger; 065 066 067 068 @Path("/accesscontrol") 069 @Consumes("application/json") 070 @Produces("application/json") 071 public class AccessControlPlugin extends PluginActivator implements AccessControlService, PreGetTopicListener, 072 PreGetAssociationListener, 073 PostCreateTopicListener, 074 PostCreateAssociationListener, 075 PostUpdateTopicListener, 076 PostUpdateAssociationListener, 077 ServiceRequestFilterListener, 078 ResourceRequestFilterListener { 079 080 // ------------------------------------------------------------------------------------------------------- Constants 081 082 // Security settings 083 private static final boolean READ_REQUIRES_LOGIN = Boolean.parseBoolean( 084 System.getProperty("dm4.security.read_requires_login", "false")); 085 private static final boolean WRITE_REQUIRES_LOGIN = Boolean.parseBoolean( 086 System.getProperty("dm4.security.write_requires_login", "true")); 087 private static final String SUBNET_FILTER = System.getProperty("dm4.security.subnet_filter", "127.0.0.1/32"); 088 // Note: the default values are required in case no config file is in effect. This applies when DM is started 089 // via feature:install from Karaf. The default values must match the values defined in global POM. 090 091 private static final String AUTHENTICATION_REALM = "DeepaMehta"; 092 093 // Associations 094 private static final String MEMBERSHIP_TYPE = "dm4.accesscontrol.membership"; 095 096 // Property URIs 097 private static String PROP_CREATOR = "dm4.accesscontrol.creator"; 098 private static String PROP_OWNER = "dm4.accesscontrol.owner"; 099 private static String PROP_MODIFIER = "dm4.accesscontrol.modifier"; 100 101 // Events 102 private static DeepaMehtaEvent POST_LOGIN_USER = new DeepaMehtaEvent(PostLoginUserListener.class) { 103 @Override 104 public void deliver(EventListener listener, Object... params) { 105 ((PostLoginUserListener) listener).postLoginUser( 106 (String) params[0] 107 ); 108 } 109 }; 110 private static DeepaMehtaEvent POST_LOGOUT_USER = new DeepaMehtaEvent(PostLogoutUserListener.class) { 111 @Override 112 public void deliver(EventListener listener, Object... params) { 113 ((PostLogoutUserListener) listener).postLogoutUser( 114 (String) params[0] 115 ); 116 } 117 }; 118 119 // ---------------------------------------------------------------------------------------------- Instance Variables 120 121 @Inject 122 private WorkspacesService wsService; 123 124 @Context 125 private HttpServletRequest request; 126 127 private Logger logger = Logger.getLogger(getClass().getName()); 128 129 // -------------------------------------------------------------------------------------------------- Public Methods 130 131 132 133 // ******************************************* 134 // *** AccessControlService Implementation *** 135 // ******************************************* 136 137 138 139 // === User Session === 140 141 @POST 142 @Path("/login") 143 @Override 144 public void login() { 145 // Note: the actual login is performed by the request filter. See requestFilter(). 146 } 147 148 @POST 149 @Path("/logout") 150 @Override 151 public void logout() { 152 _logout(request); 153 // 154 // For a "private" DeepaMehta installation: emulate a HTTP logout by forcing the webbrowser to bring up its 155 // login dialog and to forget the former Authorization information. The user is supposed to press "Cancel". 156 // The login dialog can't be used to login again. 157 if (READ_REQUIRES_LOGIN) { 158 throw401Unauthorized(); 159 } 160 } 161 162 // --- 163 164 @GET 165 @Path("/user") 166 @Produces("text/plain") 167 @Override 168 public String getUsername() { 169 try { 170 HttpSession session = request.getSession(false); // create=false 171 if (session == null) { 172 return null; 173 } 174 return username(session); 175 } catch (IllegalStateException e) { 176 // Note: if not invoked through network no request (and thus no session) is available. 177 // This happens e.g. while starting up. 178 return null; // user is unknown 179 } 180 } 181 182 183 184 // === User Accounts === 185 186 @POST 187 @Path("/user_account") 188 @Transactional 189 @Override 190 public Topic createUserAccount(Credentials cred) { 191 String username = cred.username; 192 logger.info("Creating user account \"" + username + "\""); 193 // 194 // 1) create user account 195 Topic userAccount = dms.createTopic(new TopicModel("dm4.accesscontrol.user_account", new ChildTopicsModel() 196 .put("dm4.accesscontrol.username", username) 197 .put("dm4.accesscontrol.password", cred.password))); 198 ChildTopics childTopics = userAccount.getChildTopics(); 199 Topic usernameTopic = childTopics.getTopic("dm4.accesscontrol.username"); 200 Topic passwordTopic = childTopics.getTopic("dm4.accesscontrol.password"); 201 // 202 // 2) create private workspace 203 Topic privateWorkspace = wsService.createWorkspace(DEFAULT_PRIVATE_WORKSPACE_NAME, null, SharingMode.PRIVATE); 204 setWorkspaceOwner(privateWorkspace, username); 205 // Note: we don't set a particular creator/modifier here as we don't want suggest that the new user's private 206 // workspace has been created by the new user itself. Instead we set the *current* user as the creator/modifier 207 // (via postCreateTopic() listener). In case of the "admin" user account the creator/modifier remain undefined 208 // as it is actually created by the system itself. 209 // 210 // 3) assign user account and password to private workspace 211 // Note: the current user has no READ access to the private workspace just created. 212 // So we must use the privileged assignToWorkspace calls here (instead of using the Workspaces service). 213 long privateWorkspaceId = privateWorkspace.getId(); 214 dms.getAccessControl().assignToWorkspace(userAccount, privateWorkspaceId); 215 dms.getAccessControl().assignToWorkspace(passwordTopic, privateWorkspaceId); 216 // 217 // 4) assign username to "System" workspace 218 Topic systemWorkspace = wsService.getWorkspace(SYSTEM_WORKSPACE_URI); 219 wsService.assignToWorkspace(usernameTopic, systemWorkspace.getId()); 220 // 221 return usernameTopic; 222 } 223 224 @Override 225 public Topic getPrivateWorkspace() { 226 String username = getUsername(); 227 if (username == null) { 228 throw new IllegalStateException("No user is logged in"); 229 } 230 // 231 Topic passwordTopic = getPasswordTopic(getUserAccount(getUsernameTopic(username))); 232 Topic workspace = wsService.getAssignedWorkspace(passwordTopic.getId()); 233 if (workspace == null) { 234 throw new RuntimeException("User \"" + username + "\" has no private workspace"); 235 } 236 return workspace; 237 } 238 239 @Override 240 public Topic getUsernameTopic(String username) { 241 return dms.getTopic("dm4.accesscontrol.username", new SimpleValue(username)); 242 } 243 244 245 246 // === Workspaces / Memberships === 247 248 @GET 249 @Path("/workspace/{workspace_id}/owner") 250 @Produces("text/plain") 251 @Override 252 public String getWorkspaceOwner(@PathParam("workspace_id") long workspaceId) { 253 // ### TODO: delegate to Core's AccessControl.getOwner()? 254 return dms.hasProperty(workspaceId, PROP_OWNER) ? (String) dms.getProperty(workspaceId, PROP_OWNER) : null; 255 } 256 257 @Override 258 public void setWorkspaceOwner(Topic workspace, String username) { 259 try { 260 workspace.setProperty(PROP_OWNER, username, true); // addToIndex=true 261 } catch (Exception e) { 262 throw new RuntimeException("Setting the workspace owner of " + info(workspace) + " failed (username=" + 263 username + ")", e); 264 } 265 } 266 267 // --- 268 269 @POST 270 @Path("/user/{username}/workspace/{workspace_id}") 271 @Transactional 272 @Override 273 public void createMembership(@PathParam("username") String username, @PathParam("workspace_id") long workspaceId) { 274 try { 275 dms.createAssociation(new AssociationModel(MEMBERSHIP_TYPE, 276 new TopicRoleModel(getUsernameTopicOrThrow(username).getId(), "dm4.core.default"), 277 new TopicRoleModel(workspaceId, "dm4.core.default") 278 )); 279 } catch (Exception e) { 280 throw new RuntimeException("Creating membership for user \"" + username + "\" and workspace " + 281 workspaceId + " failed", e); 282 } 283 } 284 285 @Override 286 public boolean isMember(String username, long workspaceId) { 287 return dms.getAccessControl().isMember(username, workspaceId); 288 } 289 290 291 292 // === Permissions === 293 294 @GET 295 @Path("/topic/{id}") 296 @Override 297 public Permissions getTopicPermissions(@PathParam("id") long topicId) { 298 return getPermissions(topicId); 299 } 300 301 @GET 302 @Path("/association/{id}") 303 @Override 304 public Permissions getAssociationPermissions(@PathParam("id") long assocId) { 305 return getPermissions(assocId); 306 } 307 308 309 310 // === Object Info === 311 312 @GET 313 @Path("/object/{id}/creator") 314 @Produces("text/plain") 315 @Override 316 public String getCreator(@PathParam("id") long objectId) { 317 return dms.hasProperty(objectId, PROP_CREATOR) ? (String) dms.getProperty(objectId, PROP_CREATOR) : null; 318 } 319 320 @GET 321 @Path("/object/{id}/modifier") 322 @Produces("text/plain") 323 @Override 324 public String getModifier(@PathParam("id") long objectId) { 325 return dms.hasProperty(objectId, PROP_MODIFIER) ? (String) dms.getProperty(objectId, PROP_MODIFIER) : null; 326 } 327 328 329 330 // === Retrieval === 331 332 @GET 333 @Path("/creator/{username}/topics") 334 @Override 335 public Collection<Topic> getTopicsByCreator(@PathParam("username") String username) { 336 return dms.getTopicsByProperty(PROP_CREATOR, username); 337 } 338 339 @GET 340 @Path("/owner/{username}/topics") 341 @Override 342 public Collection<Topic> getTopicsByOwner(@PathParam("username") String username) { 343 return dms.getTopicsByProperty(PROP_OWNER, username); 344 } 345 346 @GET 347 @Path("/creator/{username}/assocs") 348 @Override 349 public Collection<Association> getAssociationsByCreator(@PathParam("username") String username) { 350 return dms.getAssociationsByProperty(PROP_CREATOR, username); 351 } 352 353 @GET 354 @Path("/owner/{username}/assocs") 355 @Override 356 public Collection<Association> getAssociationsByOwner(@PathParam("username") String username) { 357 return dms.getAssociationsByProperty(PROP_OWNER, username); 358 } 359 360 361 362 // **************************** 363 // *** Hook Implementations *** 364 // **************************** 365 366 367 368 @Override 369 public void init() { 370 logger.info("Security settings:" + 371 "\ndm4.security.read_requires_login=" + READ_REQUIRES_LOGIN + 372 "\ndm4.security.write_requires_login=" + WRITE_REQUIRES_LOGIN + 373 "\ndm4.security.subnet_filter=\"" + SUBNET_FILTER + "\""); 374 } 375 376 377 378 // ******************************** 379 // *** Listener Implementations *** 380 // ******************************** 381 382 383 384 @Override 385 public void preGetTopic(long topicId) { 386 checkReadPermission(topicId); 387 } 388 389 @Override 390 public void preGetAssociation(long assocId) { 391 checkReadPermission(assocId); 392 // 393 long[] playerIds = dms.getPlayerIds(assocId); 394 checkReadPermission(playerIds[0]); 395 checkReadPermission(playerIds[1]); 396 } 397 398 // --- 399 400 @Override 401 public void postCreateTopic(Topic topic) { 402 String typeUri = topic.getTypeUri(); 403 if (typeUri.equals("dm4.workspaces.workspace")) { 404 setWorkspaceOwner(topic); 405 } else if (typeUri.equals("dm4.webclient.search")) { 406 assignSearchTopic(topic); 407 } 408 // 409 setCreatorAndModifier(topic); 410 } 411 412 @Override 413 public void postCreateAssociation(Association assoc) { 414 setCreatorAndModifier(assoc); 415 } 416 417 // --- 418 419 // ### TODO: revise/drop this method. Meanwhile a user account is created via dialog. 420 @Override 421 public void postUpdateTopic(Topic topic, TopicModel newModel, TopicModel oldModel) { 422 if (topic.getTypeUri().equals("dm4.accesscontrol.user_account")) { 423 Topic usernameTopic = topic.getChildTopics().getTopic("dm4.accesscontrol.username"); 424 Topic passwordTopic = topic.getChildTopics().getTopic("dm4.accesscontrol.password"); 425 String newUsername = usernameTopic.getSimpleValue().toString(); 426 TopicModel oldUsernameTopic = oldModel.getChildTopicsModel().getTopic("dm4.accesscontrol.username", 427 null); 428 String oldUsername = oldUsernameTopic != null ? oldUsernameTopic.getSimpleValue().toString() : ""; 429 if (!newUsername.equals(oldUsername)) { 430 // 431 if (!oldUsername.equals("")) { 432 throw new RuntimeException("Changing a Username is not supported (tried \"" + oldUsername + 433 "\" -> \"" + newUsername + "\")"); 434 } 435 // 436 logger.info("### Username has changed from \"" + oldUsername + "\" -> \"" + newUsername + 437 "\". Setting \"" + newUsername + "\" as the new owner of 3 topics:\n" + 438 " - User Account topic (ID " + topic.getId() + ")\n" + 439 " - Username topic (ID " + usernameTopic.getId() + ")\n" + 440 " - Password topic (ID " + passwordTopic.getId() + ")"); 441 // ### setOwner(topic, newUsername); 442 // ### setOwner(usernameTopic, newUsername); 443 // ### setOwner(passwordTopic, newUsername); 444 } 445 } 446 // 447 setModifier(topic); 448 } 449 450 @Override 451 public void postUpdateAssociation(Association assoc, AssociationModel oldModel) { 452 if (isMembership(assoc.getModel())) { 453 if (isMembership(oldModel)) { 454 // ### TODO? 455 } else { 456 wsService.assignToWorkspace(assoc, assoc.getTopicByType("dm4.workspaces.workspace").getId()); 457 } 458 } else if (isMembership(oldModel)) { 459 // ### TODO? 460 } 461 // 462 setModifier(assoc); 463 } 464 465 // --- 466 467 @Override 468 public void serviceRequestFilter(ContainerRequest containerRequest) { 469 // Note: we pass the injected HttpServletRequest 470 requestFilter(request); 471 } 472 473 @Override 474 public void resourceRequestFilter(HttpServletRequest servletRequest) { 475 // Note: for the resource filter no HttpServletRequest is injected 476 requestFilter(servletRequest); 477 } 478 479 480 481 // ------------------------------------------------------------------------------------------------- Private Methods 482 483 private Topic getUserAccount(Topic usernameTopic) { 484 return usernameTopic.getRelatedTopic("dm4.core.composition", "dm4.core.child", "dm4.core.parent", 485 "dm4.accesscontrol.user_account"); 486 } 487 488 private Topic getPasswordTopic(Topic userAccount) { 489 return userAccount.getChildTopics().getTopic("dm4.accesscontrol.password"); 490 } 491 492 private Topic getUsernameTopicOrThrow(String username) { 493 Topic usernameTopic = getUsernameTopic(username); 494 if (usernameTopic == null) { 495 throw new RuntimeException("User \"" + username + "\" does not exist"); 496 } 497 return usernameTopic; 498 } 499 500 private boolean isMembership(AssociationModel assoc) { 501 return assoc.getTypeUri().equals(MEMBERSHIP_TYPE); 502 } 503 504 private void assignSearchTopic(Topic searchTopic) { 505 try { 506 Topic workspace; 507 if (getUsername() != null) { 508 workspace = getPrivateWorkspace(); 509 } else { 510 workspace = wsService.getWorkspace(WorkspacesService.DEEPAMEHTA_WORKSPACE_URI); 511 } 512 wsService.assignToWorkspace(searchTopic, workspace.getId()); 513 } catch (Exception e) { 514 throw new RuntimeException("Assigning search topic to workspace failed", e); 515 } 516 } 517 518 519 520 // === Request Filter === 521 522 private void requestFilter(HttpServletRequest request) { 523 logger.fine("##### " + request.getMethod() + " " + request.getRequestURL() + 524 "\n ##### \"Authorization\"=\"" + request.getHeader("Authorization") + "\"" + 525 "\n ##### " + info(request.getSession(false))); // create=false 526 // 527 checkRequestOrigin(request); // throws WebApplicationException 528 checkAuthorization(request); // throws WebApplicationException 529 } 530 531 // --- 532 533 private void checkRequestOrigin(HttpServletRequest request) { 534 String remoteAddr = request.getRemoteAddr(); 535 boolean allowed = JavaUtils.isInRange(remoteAddr, SUBNET_FILTER); 536 // 537 logger.fine("Remote address=\"" + remoteAddr + "\", dm4.security.subnet_filter=\"" + SUBNET_FILTER + 538 "\" => " + (allowed ? "ALLOWED" : "FORBIDDEN")); 539 // 540 if (!allowed) { 541 throw403Forbidden(); // throws WebApplicationException 542 } 543 } 544 545 private void checkAuthorization(HttpServletRequest request) { 546 boolean authorized; 547 if (request.getSession(false) != null) { // create=false 548 authorized = true; 549 } else { 550 String authHeader = request.getHeader("Authorization"); 551 if (authHeader != null) { 552 // Note: if login fails we are NOT authorized, even if no login is required 553 authorized = tryLogin(new Credentials(authHeader), request); 554 } else { 555 authorized = !isLoginRequired(request); 556 } 557 } 558 // 559 if (!authorized) { 560 throw401Unauthorized(); // throws WebApplicationException 561 } 562 } 563 564 // --- 565 566 private boolean isLoginRequired(HttpServletRequest request) { 567 return request.getMethod().equals("GET") ? READ_REQUIRES_LOGIN : WRITE_REQUIRES_LOGIN; 568 } 569 570 /** 571 * Checks weather the credentials are valid and if so logs the user in. 572 * 573 * @return true if the credentials are valid. 574 */ 575 private boolean tryLogin(Credentials cred, HttpServletRequest request) { 576 String username = cred.username; 577 if (checkCredentials(cred)) { 578 logger.info("##### Logging in as \"" + username + "\" => SUCCESSFUL!"); 579 _login(username, request); 580 return true; 581 } else { 582 logger.info("##### Logging in as \"" + username + "\" => FAILED!"); 583 return false; 584 } 585 } 586 587 private boolean checkCredentials(Credentials cred) { 588 return dms.getAccessControl().checkCredentials(cred); 589 } 590 591 // --- 592 593 private void _login(String username, HttpServletRequest request) { 594 HttpSession session = request.getSession(); 595 session.setAttribute("username", username); 596 logger.info("##### Creating new " + info(session)); 597 // 598 dms.fireEvent(POST_LOGIN_USER, username); 599 } 600 601 private void _logout(HttpServletRequest request) { 602 HttpSession session = request.getSession(false); // create=false 603 String username = username(session); // save username before invalidating 604 logger.info("##### Logging out from " + info(session)); 605 // 606 session.invalidate(); 607 // 608 dms.fireEvent(POST_LOGOUT_USER, username); 609 } 610 611 // --- 612 613 private String username(HttpSession session) { 614 String username = (String) session.getAttribute("username"); 615 if (username == null) { 616 throw new RuntimeException("Session data inconsistency: \"username\" attribute is missing"); 617 } 618 return username; 619 } 620 621 // --- 622 623 private void throw401Unauthorized() { 624 // Note: a non-private DM installation (read_requires_login=false) utilizes DM's login dialog and must suppress 625 // the browser's login dialog. To suppress the browser's login dialog a contrived authentication scheme "xBasic" 626 // is used (see http://loudvchar.blogspot.ca/2010/11/avoiding-browser-popup-for-401.html) 627 String authScheme = READ_REQUIRES_LOGIN ? "Basic" : "xBasic"; 628 throw new WebApplicationException(Response.status(Status.UNAUTHORIZED) 629 .header("WWW-Authenticate", authScheme + " realm=" + AUTHENTICATION_REALM) 630 .header("Content-Type", "text/html") // for text/plain (default) Safari provides no Web Console 631 .entity("You're not authorized. Sorry.") 632 .build()); 633 } 634 635 private void throw403Forbidden() { 636 throw new WebApplicationException(Response.status(Status.FORBIDDEN) 637 .header("Content-Type", "text/html") // for text/plain (default) Safari provides no Web Console 638 .entity("Access is forbidden. Sorry.") 639 .build()); 640 } 641 642 643 644 // === Setup Access Control === 645 646 /** 647 * Sets the logged in user as the creator/modifier of the given object. 648 * <p> 649 * If no user is logged in, nothing is performed. 650 */ 651 private void setCreatorAndModifier(DeepaMehtaObject object) { 652 try { 653 String username = getUsername(); 654 // Note: when no user is logged in we do NOT fallback to the default user for the access control setup. 655 // This would not help in gaining data consistency because the topics/associations created so far 656 // (BEFORE the Access Control plugin is activated) would still have no access control setup. 657 // Note: for types the situation is different. The type-introduction mechanism (see introduceTopicType() 658 // handler above) ensures EVERY type is catched (regardless of plugin activation order). For instances on 659 // the other hand we don't have such a mechanism (and don't want one either). 660 if (username == null) { 661 logger.fine("Setting the creator/modifier of " + info(object) + " ABORTED -- no user is logged in"); 662 return; 663 } 664 // 665 setCreatorAndModifier(object, username); 666 } catch (Exception e) { 667 throw new RuntimeException("Setting the creator/modifier of " + info(object) + " failed", e); 668 } 669 } 670 671 /** 672 * @param username must not be null. 673 */ 674 private void setCreatorAndModifier(DeepaMehtaObject object, String username) { 675 setCreator(object, username); 676 setModifier(object, username); 677 } 678 679 // --- 680 681 /** 682 * Sets the creator of a topic or an association. 683 */ 684 private void setCreator(DeepaMehtaObject object, String username) { 685 try { 686 object.setProperty(PROP_CREATOR, username, true); // addToIndex=true 687 } catch (Exception e) { 688 throw new RuntimeException("Setting the creator of " + info(object) + " failed (username=" + username + ")", 689 e); 690 } 691 } 692 693 // --- 694 695 private void setModifier(DeepaMehtaObject object) { 696 String username = getUsername(); 697 // Note: when a plugin topic is updated there is no user logged in yet. 698 if (username == null) { 699 return; 700 } 701 // 702 setModifier(object, username); 703 } 704 705 private void setModifier(DeepaMehtaObject object, String username) { 706 object.setProperty(PROP_MODIFIER, username, false); // addToIndex=false 707 } 708 709 // --- 710 711 private void setWorkspaceOwner(Topic workspace) { 712 String username = getUsername(); 713 // Note: username is null if the Access Control plugin is activated already 714 // when a 3rd-party plugin creates a workspace at install-time. 715 if (username == null) { 716 return; 717 } 718 // 719 setWorkspaceOwner(workspace, username); 720 } 721 722 723 724 // === Calculate Permissions === 725 726 /** 727 * @param objectId a topic ID, or an association ID 728 */ 729 private void checkReadPermission(long objectId) { 730 String username = getUsername(); 731 if (!hasPermission(username, Operation.READ, objectId)) { 732 throw new AccessControlException(userInfo(username) + " has no READ permission for object " + objectId); 733 } 734 } 735 736 /** 737 * @param objectId a topic ID, or an association ID. 738 */ 739 private Permissions getPermissions(long objectId) { 740 return new Permissions().add(Operation.WRITE, hasPermission(getUsername(), Operation.WRITE, objectId)); 741 } 742 743 /** 744 * Checks if a user is permitted to perform an operation on an object (topic or association). 745 * 746 * @param username the logged in user, or <code>null</code> if no user is logged in. 747 * @param objectId a topic ID, or an association ID. 748 * 749 * @return <code>true</code> if permission is granted, <code>false</code> otherwise. 750 */ 751 private boolean hasPermission(String username, Operation operation, long objectId) { 752 return dms.getAccessControl().hasPermission(username, operation, objectId); 753 } 754 755 756 757 // === Logging === 758 759 private String info(DeepaMehtaObject object) { 760 if (object instanceof TopicType) { 761 return "topic type \"" + object.getUri() + "\" (id=" + object.getId() + ")"; 762 } else if (object instanceof AssociationType) { 763 return "association type \"" + object.getUri() + "\" (id=" + object.getId() + ")"; 764 } else if (object instanceof Topic) { 765 return "topic " + object.getId() + " (typeUri=\"" + object.getTypeUri() + "\", uri=\"" + object.getUri() + 766 "\")"; 767 } else if (object instanceof Association) { 768 return "association " + object.getId() + " (typeUri=\"" + object.getTypeUri() + "\")"; 769 } else { 770 throw new RuntimeException("Unexpected object: " + object); 771 } 772 } 773 774 private String userInfo(String username) { 775 return "user " + (username != null ? "\"" + username + "\"" : "<anonymous>"); 776 } 777 778 private String info(HttpSession session) { 779 return "session" + (session != null ? " " + session.getId() + 780 " (username=" + username(session) + ")" : ": null"); 781 } 782 783 private String info(HttpServletRequest request) { 784 StringBuilder info = new StringBuilder(); 785 info.append(" " + request.getMethod() + " " + request.getRequestURI() + "\n"); 786 Enumeration<String> e1 = request.getHeaderNames(); 787 while (e1.hasMoreElements()) { 788 String name = e1.nextElement(); 789 info.append("\n " + name + ":"); 790 Enumeration<String> e2 = request.getHeaders(name); 791 while (e2.hasMoreElements()) { 792 String header = e2.nextElement(); 793 info.append(" " + header); 794 } 795 } 796 return info.toString(); 797 } 798 }